mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-13 18:46:37 +00:00
Compare commits
3
Commits
v3.8.0
...
768517a658
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
768517a658 | ||
|
|
5709036e3c | ||
|
|
b26b31e320 |
@@ -47,6 +47,28 @@ Web interface:
|
|||||||
JSON API saves are unaffected. Lets plugins keep deprecated or internal keys
|
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`.
|
declared, e.g. countdown's row `id` and weather's `api_key` / `radar_zoom`.
|
||||||
See `docs/widget-guide.md`.
|
See `docs/widget-guide.md`.
|
||||||
|
- Display settings no longer silently cut values on save: columns were capped
|
||||||
|
at 128, chain length at 24 and PWM LSB nanoseconds at 500. Rows, columns and
|
||||||
|
chain length now have no upper limit (the current rgbmatrix library still
|
||||||
|
rejects more than 64 rows per panel); rows must be even and at least 8,
|
||||||
|
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
|
||||||
|
re-saves) as 3, 1 or 120, and the refresh cap accepts 0 (no cap). The config
|
||||||
|
API rejects out-of-range or non-integer `rows`, `cols`, `chain_length`,
|
||||||
|
`parallel`, `brightness`, `scan_mode`, `pwm_bits`, `pwm_dither_bits`,
|
||||||
|
`pwm_lsb_nanoseconds`, `limit_refresh_rate_hz`, `row_address_type`,
|
||||||
|
`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
|
||||||
|
and completed: `panel_type` and `rp1_rio` are documented,
|
||||||
|
`show_refresh_rate` prints to the console rather than drawing on the panel,
|
||||||
|
PWM dither bits raise the refresh rate rather than lowering it, and every
|
||||||
|
numeric setting states its range.
|
||||||
|
- 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
|
## 3.4.0
|
||||||
|
|
||||||
|
|||||||
@@ -494,15 +494,20 @@ These settings control the physical hardware configuration and how the matrix is
|
|||||||
- **`rows`** (integer, default: 32)
|
- **`rows`** (integer, default: 32)
|
||||||
- Number of LED rows (vertical pixels) in each panel
|
- Number of LED rows (vertical pixels) in each panel
|
||||||
- Common values: 16, 32, 48, 64
|
- Common values: 16, 32, 48, 64
|
||||||
|
- Must be an even number, at least 8. LEDMatrix sets no upper limit, but the
|
||||||
|
current rgbmatrix library rejects more than 64 rows per panel — the display
|
||||||
|
then won't start (see Troubleshooting Display Settings below)
|
||||||
- Must match your physical panel configuration
|
- Must match your physical panel configuration
|
||||||
|
|
||||||
- **`cols`** (integer, default: 64)
|
- **`cols`** (integer, default: 64)
|
||||||
- Number of LED columns (horizontal pixels) in each panel
|
- Number of LED columns (horizontal pixels) in each panel
|
||||||
- Common values: 32, 64, 96, 128
|
- Common values: 32, 64, 96, 128
|
||||||
|
- At least 16, with no upper limit
|
||||||
- Must match your physical panel configuration
|
- Must match your physical panel configuration
|
||||||
|
|
||||||
- **`chain_length`** (integer, default: 2)
|
- **`chain_length`** (integer, default: 2)
|
||||||
- Number of LED panels chained together horizontally
|
- 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 2 panels side-by-side, set to 2
|
||||||
- If you have 4 panels in a row, set to 4
|
- If you have 4 panels in a row, set to 4
|
||||||
- Total display width = `cols × chain_length`
|
- Total display width = `cols × chain_length`
|
||||||
@@ -511,13 +516,14 @@ These settings control the physical hardware configuration and how the matrix is
|
|||||||
- Number of parallel chains (panels stacked vertically)
|
- Number of parallel chains (panels stacked vertically)
|
||||||
- Use 1 for a single row of panels
|
- Use 1 for a single row of panels
|
||||||
- Use 2 if you have panels stacked in two rows
|
- 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`
|
- Total display height = `rows × parallel`
|
||||||
|
|
||||||
#### Brightness and Visual Settings
|
#### Brightness and Visual Settings
|
||||||
|
|
||||||
- **`brightness`** (integer, 0-100, default: 90)
|
- **`brightness`** (integer, 1-100, default: 90)
|
||||||
- Display brightness level
|
- 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
|
- Recommended: 70-90 for indoor use, 90-100 for bright environments
|
||||||
- Very high brightness may cause distortion or require more power
|
- Very high brightness may cause distortion or require more power
|
||||||
|
|
||||||
@@ -527,52 +533,52 @@ These settings control the physical hardware configuration and how the matrix is
|
|||||||
- Specifies which GPIO pin mapping to use for your hardware
|
- Specifies which GPIO pin mapping to use for your hardware
|
||||||
- **`"adafruit-hat-pwm"`**: Use this for Adafruit RGB Matrix Bonnet/HAT WITH the jumper mod (PWM enabled). This is the recommended setting for Adafruit hardware with the PWM jumper soldered.
|
- **`"adafruit-hat-pwm"`**: Use this for Adafruit RGB Matrix Bonnet/HAT WITH the jumper mod (PWM enabled). This is the recommended setting for Adafruit hardware with the PWM jumper soldered.
|
||||||
- **`"adafruit-hat"`**: Use this for Adafruit RGB Matrix Bonnet/HAT WITHOUT the jumper mod (no PWM). Remove `-pwm` from the value if you did not solder the jumper.
|
- **`"adafruit-hat"`**: Use this for Adafruit RGB Matrix Bonnet/HAT WITHOUT the jumper mod (no PWM). Remove `-pwm` from the value if you did not solder the jumper.
|
||||||
- **`"regular"`**: Standard GPIO pin mapping for direct GPIO connections (Generic)
|
- **`"regular"`**: Standard GPIO pin mapping for direct GPIO connections (Generic). Also the right choice for the Adafruit Triple LED Matrix Bonnet
|
||||||
- **`"regular-pi1"`**: Standard GPIO pin mapping for Raspberry Pi 1 (older hardware or non-standard hat mapping)
|
- **`"regular-pi1"`**: Standard GPIO pin mapping for Raspberry Pi 1 (older hardware or non-standard hat mapping)
|
||||||
- Choose the option that matches your specific hardware setup, if aren't sure try them all.
|
- Choose the option that matches your specific hardware setup, if aren't sure try them all.
|
||||||
|
- Hardware pulsing (see `disable_hardware_pulsing`) needs the panel's OE line on GPIO 18, which `adafruit-hat-pwm` and `regular` provide and `adafruit-hat` does not
|
||||||
|
|
||||||
#### PWM (Pulse Width Modulation) Settings
|
#### PWM (Pulse Width Modulation) Settings
|
||||||
|
|
||||||
These settings affect color fidelity and smoothness of color transitions:
|
These settings affect color fidelity and smoothness of color transitions:
|
||||||
|
|
||||||
- **`pwm_bits`** (integer, default: 9)
|
- **`pwm_bits`** (integer, 1-11, default: 9)
|
||||||
- Number of bits used for PWM (affects color depth)
|
- Color depth per channel: how many brightness levels each LED gets
|
||||||
- Higher values (9-11) = more color levels, smoother gradients
|
- Higher values (9-11) = more color levels, smoother gradients, lower refresh rate
|
||||||
- Lower values (7-8) = fewer color levels, but may improve stability on some hardware
|
- Lower values (7-8) = the subtlest shades are dropped for a higher refresh rate; `1` gives 8 colors
|
||||||
- Range: 1-11, recommended: 9-10
|
- Recommended: 9-10
|
||||||
|
|
||||||
- **`pwm_dither_bits`** (integer, default: 1)
|
- **`pwm_dither_bits`** (integer, 0-2, default: 1)
|
||||||
- Additional dithering bits for smoother color transitions
|
- Time-dithers the lowest color bits: their brightness comes from showing them on only some frames
|
||||||
- Helps reduce color banding in gradients
|
- Raises the refresh rate; the cost is that dark shades can shimmer slightly
|
||||||
- Higher values (1-2) = smoother gradients but may impact performance
|
- `0` = steadiest dim colors, `2` = fastest
|
||||||
- Range: 0-2, recommended: 1
|
- The rgbmatrix library accepts only 0-2; a higher value stops the display starting
|
||||||
|
|
||||||
- **`pwm_lsb_nanoseconds`** (integer, default: 130)
|
- **`pwm_lsb_nanoseconds`** (integer, 50-3000, default: 130)
|
||||||
- Least significant bit timing in nanoseconds
|
- On-time of the least significant color bit; each higher bit doubles it
|
||||||
- Controls the base timing for PWM signals
|
- Lower values = higher refresh rate, but can cost color accuracy or add ghosting on some panels
|
||||||
- Lower values = faster PWM, higher values = slower PWM
|
- Higher values = less ghosting (faint trails behind bright text on black), lower refresh rate
|
||||||
- Typical range: 100-300 nanoseconds
|
- Typical range: 100-300 nanoseconds
|
||||||
- May need adjustment if you see flickering or color issues
|
|
||||||
|
|
||||||
#### Advanced Hardware Settings
|
#### Advanced Hardware Settings
|
||||||
|
|
||||||
- **`scan_mode`** (integer, default: 0)
|
- **`scan_mode`** (integer, 0-1, default: 0)
|
||||||
- Panel scan mode (how rows are addressed)
|
- Order the rows are refreshed in: `0` = progressive, `1` = interlaced
|
||||||
- Common values: 0 (progressive), 1 (interlaced)
|
- Interlaced can look a little smoother when the refresh rate is very low, but usually shows a comb effect on anything moving
|
||||||
- Most panels use 0, but some require 1
|
- Leave at `0` unless you are tuning a slow setup
|
||||||
- Check your panel datasheet if colors appear incorrect
|
|
||||||
|
|
||||||
- **`limit_refresh_rate_hz`** (integer, default: 100)
|
- **`limit_refresh_rate_hz`** (integer, default: 100)
|
||||||
- Maximum refresh rate in Hz (frames per second)
|
- Caps the panel refresh rate in Hz; `0` = no cap
|
||||||
- Caps the refresh rate for better stability
|
- A steady cap reduces flicker caused by other activity on the Pi, and in camera recordings
|
||||||
- Lower values (60-80) = more stable, less CPU usage
|
- Scroll speeds are worked out against this value (against 100 Hz when it is `0`), so a cap the panel can actually hold keeps scrolling even
|
||||||
- Higher values (100-120) = smoother animations, more CPU usage
|
- If the key is missing from the config, `DisplayManager` uses 90
|
||||||
- Recommended: 80-100 for most setups
|
- Recommended: 80-120. `sudo python3 scripts/scroll_speeds.py --measure` reports the rate your panel really achieves
|
||||||
|
|
||||||
- **`disable_hardware_pulsing`** (boolean, default: false)
|
- **`disable_hardware_pulsing`** (boolean, default: false)
|
||||||
- Disables hardware pulsing (usually leave as false)
|
- `false` = the Pi's hardware PWM times each brightness pulse; `true` = software timing
|
||||||
- Set to `true` only if you experience timing issues
|
- Leave `false` where possible. Software timing is less exact, so a row, or the whole panel, can briefly flash brighter
|
||||||
- Most users should leave this as `false`
|
- Hardware pulsing needs the panel's OE line on GPIO 18 (`adafruit-hat-pwm`, `regular`, the Adafruit Triple LED Matrix Bonnet). With `adafruit-hat` the library uses software timing anyway
|
||||||
|
- It also needs the Pi's onboard sound driver (`snd_bcm2835`) disabled, which `first_time_install.sh` does. Set `true` only if you need the Pi's own audio
|
||||||
|
|
||||||
- **`inverse_colors`** (boolean, default: false)
|
- **`inverse_colors`** (boolean, default: false)
|
||||||
- Inverts all colors (red becomes cyan, etc.)
|
- Inverts all colors (red becomes cyan, etc.)
|
||||||
@@ -580,9 +586,9 @@ These settings affect color fidelity and smoothness of color transitions:
|
|||||||
- Set to `true` only if colors appear inverted
|
- Set to `true` only if colors appear inverted
|
||||||
|
|
||||||
- **`show_refresh_rate`** (boolean, default: false)
|
- **`show_refresh_rate`** (boolean, default: false)
|
||||||
- Displays the current refresh rate on the matrix (for debugging)
|
- Prints the live refresh rate to the console; nothing is drawn on the panel
|
||||||
- Set to `true` to see FPS on the display
|
- Readable when you stop the service and run `sudo python3 run.py` in a terminal; under the service the output is buffered
|
||||||
- Useful for troubleshooting performance issues
|
- `sudo python3 scripts/scroll_speeds.py --measure` is an easier way to see the real refresh rate
|
||||||
|
|
||||||
#### Advanced Panel Configuration (Advanced Users Only)
|
#### Advanced Panel Configuration (Advanced Users Only)
|
||||||
|
|
||||||
@@ -592,6 +598,7 @@ These settings are typically only needed for non-standard panels or custom confi
|
|||||||
- Color channel order for your LED panel
|
- Color channel order for your LED panel
|
||||||
- Common values: "RGB", "RBG", "GRB", "GBR", "BRG", "BGR"
|
- Common values: "RGB", "RBG", "GRB", "GBR", "BRG", "BGR"
|
||||||
- Most panels use "RGB", but some use "GRB" or other orders
|
- Most panels use "RGB", but some use "GRB" or other orders
|
||||||
|
- If red shows as blue, try "BGR" (the Waveshare 96x48 V2 needs it)
|
||||||
- Check your panel datasheet if colors appear wrong
|
- Check your panel datasheet if colors appear wrong
|
||||||
|
|
||||||
- **`pixel_mapper_config`** (string, default: "")
|
- **`pixel_mapper_config`** (string, default: "")
|
||||||
@@ -612,29 +619,56 @@ These settings are typically only needed for non-standard panels or custom confi
|
|||||||
- **`row_address_type`** (integer, default: 0)
|
- **`row_address_type`** (integer, default: 0)
|
||||||
- How rows are addressed on the panel
|
- How rows are addressed on the panel
|
||||||
- Most panels use 0 (direct addressing)
|
- 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
|
- Check your panel datasheet if display appears corrupted
|
||||||
|
|
||||||
- **`multiplexing`** (integer, default: 0)
|
- **`multiplexing`** (integer, 0-22, default: 0)
|
||||||
- Panel multiplexing type
|
- How pixels are wired on outdoor/specialty panels (P10, P8, P4 and P3 outdoor modules and similar) whose LEDs aren't laid out in straight rows
|
||||||
- 0 = no multiplexing (standard panels)
|
- `0` = direct (standard indoor panels)
|
||||||
- Higher values for panels with different multiplexing schemes
|
- `1` Stripe, `2` Checkered, `3` Spiral, `4` ZStripe, `5` ZnMirrorZStripe,
|
||||||
- Check your panel datasheet for the correct value
|
`6` Coreman, `7` Kaler2Scan, `8` ZStripeUneven, `9` P10-128x4-Z,
|
||||||
|
`10` QiangLiQ8, `11` InversedZStripe, `12`–`14` P10Outdoor1R1G1B v1–v3,
|
||||||
|
`15` P10CoremanMapper, `16` P8Outdoor1R1G1B, `17` FlippedStripe,
|
||||||
|
`18` P10-32x16-HalfScan, `19` P10-32x16-QuarterScan, `20` P3Outdoor-64x64,
|
||||||
|
`21` DoubleZMultiplex, `22` P4Outdoor-80x40
|
||||||
|
- If the image is scrambled in a repeating pattern, try the value named after your panel first
|
||||||
|
|
||||||
|
- **`panel_type`** (string, default: `""`)
|
||||||
|
- Sends a start-up initialization sequence to driver chips that need one
|
||||||
|
- `""` = Standard (no initialization) — right for most panels, including FM6124 / FM6124D / FM6124DJ
|
||||||
|
- `"FM6126A"` or `"FM6127"` for panels with those chips; try `"FM6126A"` if the panel stays dark or lights only the first pixel on Standard
|
||||||
|
|
||||||
### Runtime Configuration (`display.runtime`)
|
### Runtime Configuration (`display.runtime`)
|
||||||
|
|
||||||
These settings control runtime behavior and GPIO timing:
|
These settings control runtime behavior and GPIO timing:
|
||||||
|
|
||||||
- **`gpio_slowdown`** (integer, default: 3)
|
- **`gpio_slowdown`** (integer, default: 3)
|
||||||
- GPIO timing slowdown factor
|
- GPIO timing slowdown factor (0-10): slows GPIO writes so the panel electronics keep up. Higher is more reliable but lowers the refresh rate
|
||||||
- **Critical setting**: Must match your Raspberry Pi model for stability
|
- **Critical setting**: depends on your Raspberry Pi model and your panel
|
||||||
- **Raspberry Pi 3**: Use 3
|
- **Raspberry Pi Zero/1**: 0-1
|
||||||
- **Raspberry Pi 4**: Use 4
|
- **Raspberry Pi 2/3**: 1-3
|
||||||
- **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 4**: 2-4 (the config template ships 3)
|
||||||
- **Raspberry Pi Zero/1**: Use 1-2
|
- **Raspberry Pi 5**: 1–3 in PIO mode (`rp1_rio: 0`, the default); start with `1` and increase if you see flickering
|
||||||
- Incorrect values can cause display corruption, flickering, or system instability
|
- Panels on `row_address_type` 5 (SM5368 row drivers) can need 6-8 on a Pi 4
|
||||||
|
- Too low: garbage, flicker or rows jumping. Too high: a lower refresh rate
|
||||||
- If you experience issues, try adjusting this value up or down by 1
|
- If you experience issues, try adjusting this value up or down by 1
|
||||||
|
|
||||||
|
- **`rp1_rio`** (integer, 0 or 1, default: 0) — Raspberry Pi 5 only
|
||||||
|
- Which driver the Pi 5's RP1 chip uses: `0` = PIO (default, less CPU), `1` = RIO (registered I/O, can reach a higher refresh rate)
|
||||||
|
- In RIO mode the effect of `gpio_slowdown` is inverted: higher values may be faster
|
||||||
|
- Ignored on a Pi 0-4, and applied only if the installed rgbmatrix library supports it
|
||||||
|
|
||||||
### Display Durations (`display.display_durations`)
|
### Display Durations (`display.display_durations`)
|
||||||
|
|
||||||
Controls how long each installed plugin stays visible in seconds before switching to the next one, keyed by plugin id.
|
Controls how long each installed plugin stays visible in seconds before switching to the next one, keyed by plugin id.
|
||||||
@@ -667,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
|
||||||
|
|
||||||
@@ -714,6 +748,14 @@ Controls how long each installed plugin stays visible in seconds before switchin
|
|||||||
- Verify `hardware_mapping` matches your HAT/connection type
|
- Verify `hardware_mapping` matches your HAT/connection type
|
||||||
- Try adjusting `gpio_slowdown`
|
- Try adjusting `gpio_slowdown`
|
||||||
- Ensure your display doesn't need the E-Addressable line
|
- 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
|
||||||
|
- 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:**
|
||||||
|
- Raise `gpio_slowdown` a step at a time (SM5368 panels on `row_address_type` 5 can need 6-8 on a Pi 4)
|
||||||
|
|
||||||
|
**A row or the whole panel briefly flashes brighter:**
|
||||||
|
- Set `disable_hardware_pulsing` to `false` (needs the OE line on GPIO 18; see `hardware_mapping`)
|
||||||
|
|
||||||
**Colors are wrong or inverted:**
|
**Colors are wrong or inverted:**
|
||||||
- Check `led_rgb_sequence` (try "GRB" if "RGB" doesn't work)
|
- Check `led_rgb_sequence` (try "GRB" if "RGB" doesn't work)
|
||||||
|
|||||||
+17
-17
@@ -51,25 +51,25 @@ in `DisplayManager` (`src/display_manager.py`, ~lines 270–295).
|
|||||||
|
|
||||||
| Key | Type / default |
|
| Key | Type / default |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `rows` / `cols` | int, `32` / `64` |
|
| `rows` / `cols` | int, `32` / `64` — rows: even, at least 8, no upper limit here (the current rgbmatrix library rejects more than 64); cols: at least 16, no upper limit |
|
||||||
| `chain_length` | int, `2` |
|
| `chain_length` | int, `2` — at least 1, no upper limit |
|
||||||
| `parallel` | int, `1` |
|
| `parallel` | int, `1` — 1–3 |
|
||||||
| `brightness` | int, `90` |
|
| `brightness` | int, `90` — 1–100 |
|
||||||
| `hardware_mapping` | string, `"adafruit-hat"` (code default `"adafruit-hat-pwm"`) |
|
| `hardware_mapping` | string, `"adafruit-hat"` (code default `"adafruit-hat-pwm"`) |
|
||||||
| `scan_mode` | int, `0` |
|
| `scan_mode` | int, `0` — `0` progressive, `1` interlaced |
|
||||||
| `pwm_bits` | int, `9` (code default 10) |
|
| `pwm_bits` | int, `9` (code default 10) — 1–11 |
|
||||||
| `pwm_dither_bits` | int, `1` |
|
| `pwm_dither_bits` | int, `1` — 0–2 |
|
||||||
| `pwm_lsb_nanoseconds` | int, `130` (code default 150) |
|
| `pwm_lsb_nanoseconds` | int, `130` (code default 150) — 50–3000 |
|
||||||
| `disable_hardware_pulsing` | bool, `false` |
|
| `disable_hardware_pulsing` | bool, `false` — `true` times brightness pulses in software (less exact); hardware pulsing needs the OE line on GPIO 18 and the Pi's onboard sound driver off |
|
||||||
| `inverse_colors` | bool, `false` |
|
| `inverse_colors` | bool, `false` |
|
||||||
| `show_refresh_rate` | bool, `false` |
|
| `show_refresh_rate` | bool, `false` — prints the refresh rate to stdout; draws nothing on the panel |
|
||||||
| `led_rgb_sequence` | string, `"RGB"` |
|
| `led_rgb_sequence` | string, `"RGB"` — `"RGB"`, `"RBG"`, `"GRB"`, `"GBR"`, `"BRG"` or `"BGR"` |
|
||||||
| `limit_refresh_rate_hz` | int, `100` (code default 90) |
|
| `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"` |
|
| `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 |
|
| `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 |
|
| `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 |
|
| `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
|
Where "code default" differs from the template value, the code default only
|
||||||
applies if the key is missing entirely from your config.
|
applies if the key is missing entirely from your config.
|
||||||
@@ -78,8 +78,8 @@ applies if the key is missing entirely from your config.
|
|||||||
|
|
||||||
| Key | Type / default | Meaning |
|
| 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) |
|
| `rp1_rio` | int, `0` | Pi 5 only: `0` = PIO (less CPU), `1` = RIO (higher refresh; `gpio_slowdown` effect inverted). Applied only if the installed matrix library supports it |
|
||||||
|
|
||||||
## `display.double_sided`
|
## `display.double_sided`
|
||||||
|
|
||||||
|
|||||||
@@ -83,10 +83,10 @@ You should see:
|
|||||||
|
|
||||||
1. Open the **Display** tab
|
1. Open the **Display** tab
|
||||||
2. Set your matrix configuration:
|
2. Set your matrix configuration:
|
||||||
- **Rows**: 32 or 64 (match your hardware)
|
- **Rows**: match your panel — commonly 32 or 64; any even number
|
||||||
- **Columns**: commonly 64 or 96; the web UI accepts any integer
|
from 8 to 64
|
||||||
in the 1–128 range, but 64 and 96 are the values the bundled
|
- **Columns**: match your panel — commonly 64 or 96; at least 16,
|
||||||
panel hardware ships with
|
with no upper limit
|
||||||
- **Chain Length**: Number of panels chained horizontally
|
- **Chain Length**: Number of panels chained horizontally
|
||||||
- **Hardware Mapping**: usually `adafruit-hat-pwm` (with the PWM jumper
|
- **Hardware Mapping**: usually `adafruit-hat-pwm` (with the PWM jumper
|
||||||
mod) or `adafruit-hat` (without). See the root README for the full list.
|
mod) or `adafruit-hat` (without). See the root README for the full list.
|
||||||
|
|||||||
@@ -132,18 +132,26 @@ require a display service restart from **Overview**.
|
|||||||
Configure your LED matrix hardware:
|
Configure your LED matrix hardware:
|
||||||
|
|
||||||
**Matrix configuration:**
|
**Matrix configuration:**
|
||||||
- `rows` — LED rows (typically 32 or 64)
|
- `rows` — LED rows per panel (typically 32 or 64; even, at least 8 — the
|
||||||
- `cols` — LED columns (typically 64 or 96)
|
current rgbmatrix library rejects more than 64)
|
||||||
|
- `cols` — LED columns per panel (typically 64 or 96; at least 16)
|
||||||
- `chain_length` — number of horizontally chained panels
|
- `chain_length` — number of horizontally chained panels
|
||||||
- `parallel` — number of parallel chains
|
- `parallel` — number of parallel chains (1–3)
|
||||||
- `hardware_mapping` — `adafruit-hat-pwm` (with PWM jumper mod),
|
- `hardware_mapping` — `adafruit-hat-pwm` (with PWM jumper mod),
|
||||||
`adafruit-hat` (without), `regular`, or `regular-pi1`
|
`adafruit-hat` (without), `regular` (direct wiring, and the Adafruit Triple
|
||||||
- `gpio_slowdown` — must match your Pi model (3 for Pi 3, 4 for Pi 4, etc.)
|
LED Matrix Bonnet), or `regular-pi1`
|
||||||
- `brightness` — 0–100%
|
- `gpio_slowdown` — depends on your Pi and panel (roughly 1–3 on a Pi 3,
|
||||||
|
2–4 on a Pi 4); raise it if rows jump or the image is garbage
|
||||||
|
- `brightness` — 1–100%
|
||||||
- `pwm_bits`, `pwm_lsb_nanoseconds`, `pwm_dither_bits` — PWM tuning
|
- `pwm_bits`, `pwm_lsb_nanoseconds`, `pwm_dither_bits` — PWM tuning
|
||||||
- Dynamic Duration — global cap for plugins that extend their display
|
- Dynamic Duration — global cap for plugins that extend their display
|
||||||
time based on content
|
time based on content
|
||||||
|
|
||||||
|
The collapsed **Advanced Hardware & Display Options** section holds
|
||||||
|
multiplexing, panel type, row address type, scan mode, PWM tuning, the
|
||||||
|
refresh-rate cap and hardware pulsing. Every field has a help tip, and the
|
||||||
|
README's Display Settings section describes each one with its allowed range.
|
||||||
|
|
||||||
**Vegas Scroll Mode:** the Display tab also has a full Vegas Scroll
|
**Vegas Scroll Mode:** the Display tab also has a full Vegas Scroll
|
||||||
Mode section — enable toggle, scroll speed, separator width, dynamic
|
Mode section — enable toggle, scroll speed, separator width, dynamic
|
||||||
duration, and related settings — so you can configure Vegas mode
|
duration, and related settings — so you can configure Vegas mode
|
||||||
|
|||||||
@@ -0,0 +1,222 @@
|
|||||||
|
"""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), with
|
||||||
|
one deliberate exception: rows has no upper bound here, although the library
|
||||||
|
currently rejects more than 64 per panel. 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), ('rows', 96), ('rows', 128),
|
||||||
|
('cols', 16), ('cols', 192), ('cols', 512),
|
||||||
|
('chain_length', 1), ('chain_length', 32),
|
||||||
|
('parallel', 3),
|
||||||
|
('row_address_type', 0), ('row_address_type', 5), ('row_address_type', 5.0),
|
||||||
|
('multiplexing', 0), ('multiplexing', 22),
|
||||||
|
('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),
|
||||||
|
('limit_refresh_rate_hz', 0), ('limit_refresh_rate_hz', 1000),
|
||||||
|
])
|
||||||
|
def test_values_in_range_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', 97), ('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),
|
||||||
|
('row_address_type', True), ('row_address_type', 5.5),
|
||||||
|
('multiplexing', -1), ('multiplexing', 23), ('multiplexing', True),
|
||||||
|
('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),
|
||||||
|
('limit_refresh_rate_hz', -1),
|
||||||
|
])
|
||||||
|
def test_values_out_of_range_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': None, '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'}),
|
||||||
|
('limit_refresh_rate_hz', {'min': '0', 'max': '1000'}),
|
||||||
|
])
|
||||||
|
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'),
|
||||||
|
('limit_refresh_rate_hz', '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'
|
||||||
@@ -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,14 +564,47 @@ 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
|
# Panel geometry, PWM and GPIO timing, held to what the rgbmatrix library
|
||||||
if 'row_address_type' in data:
|
# 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, chain_length and
|
||||||
|
# limit_refresh_rate_hz (0 = no cap) have no upper bound in the
|
||||||
|
# library. rows has none here by choice: the library currently
|
||||||
|
# rejects more than 64 per panel, and that limit is left to it so a
|
||||||
|
# library that lifts it needs no change 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]
|
||||||
|
kind = "an even integer" if even else "an integer"
|
||||||
|
if high is None:
|
||||||
|
allowed = f"{kind} of at least {low}"
|
||||||
|
else:
|
||||||
|
allowed = f"{kind} 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:
|
try:
|
||||||
rat_val = int(data['row_address_type'])
|
value = int(raw)
|
||||||
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
|
|
||||||
except (ValueError, TypeError, OverflowError):
|
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 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, None, 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),
|
||||||
|
('limit_refresh_rate_hz', 0, None, False),
|
||||||
|
('row_address_type', 0, 5, False), ('multiplexing', 0, 22, 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
|
# Handle hardware settings
|
||||||
for field in ['rows', 'cols', 'chain_length', 'parallel', 'brightness', 'hardware_mapping', 'scan_mode',
|
for field in ['rows', 'cols', 'chain_length', 'parallel', 'brightness', 'hardware_mapping', 'scan_mode',
|
||||||
|
|||||||
@@ -41,46 +41,44 @@
|
|||||||
|
|
||||||
<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="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">
|
<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 and be an even number, at least 8. There is no upper limit here, but the current rgbmatrix library rejects more than 64 rows per panel, and the display will not start.', 'Rows') }}</label>
|
||||||
<input type="number"
|
<input type="number"
|
||||||
id="rows"
|
id="rows"
|
||||||
name="rows"
|
name="rows"
|
||||||
value="{{ main_config.display.hardware.rows or 32 }}"
|
value="{{ main_config.display.hardware.rows or 32 }}"
|
||||||
min="1"
|
min="8"
|
||||||
max="128"
|
step="2"
|
||||||
class="form-control">
|
class="form-control">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" id="setting-display-cols" data-setting-key="display.hardware.cols">
|
<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"
|
<input type="number"
|
||||||
id="cols"
|
id="cols"
|
||||||
name="cols"
|
name="cols"
|
||||||
value="{{ main_config.display.hardware.cols or 64 }}"
|
value="{{ main_config.display.hardware.cols or 64 }}"
|
||||||
min="1"
|
min="16"
|
||||||
max="128"
|
|
||||||
class="form-control">
|
class="form-control">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" id="setting-display-chain_length" data-setting-key="display.hardware.chain_length">
|
<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"
|
<input type="number"
|
||||||
id="chain_length"
|
id="chain_length"
|
||||||
name="chain_length"
|
name="chain_length"
|
||||||
value="{{ main_config.display.hardware.chain_length or 2 }}"
|
value="{{ main_config.display.hardware.chain_length or 2 }}"
|
||||||
min="1"
|
min="1"
|
||||||
max="24"
|
|
||||||
class="form-control">
|
class="form-control">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" id="setting-display-parallel" data-setting-key="display.hardware.parallel">
|
<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"
|
<input type="number"
|
||||||
id="parallel"
|
id="parallel"
|
||||||
name="parallel"
|
name="parallel"
|
||||||
value="{{ main_config.display.hardware.parallel or 1 }}"
|
value="{{ main_config.display.hardware.parallel or 1 }}"
|
||||||
min="1"
|
min="1"
|
||||||
max="4"
|
max="3"
|
||||||
class="form-control">
|
class="form-control">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -108,7 +106,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" id="setting-display-hardware_mapping" data-setting-key="display.hardware.hardware_mapping">
|
<div class="form-group" id="setting-display-hardware_mapping" data-setting-key="display.hardware.hardware_mapping">
|
||||||
<label for="hardware_mapping" class="block text-sm font-medium text-gray-700">Hardware Mapping{{ ui.help_tip('How the LED panel is wired to the Pi.\nUse "Adafruit HAT PWM" for an Adafruit HAT/Bonnet with the PWM solder mod; "Adafruit HAT" without it; "Regular" for direct GPIO wiring.', 'Hardware Mapping') }}</label>
|
<label for="hardware_mapping" class="block text-sm font-medium text-gray-700">Hardware Mapping{{ ui.help_tip('How the LED panel is wired to the Pi.\nUse "Adafruit HAT PWM" for an Adafruit RGB Matrix HAT/Bonnet with the PWM solder mod; "Adafruit HAT" without it (no hardware pulsing, so expect a little more flicker); "Regular" for direct GPIO wiring and for the Adafruit Triple LED Matrix Bonnet.', 'Hardware Mapping') }}</label>
|
||||||
<select id="hardware_mapping" name="hardware_mapping" class="form-control">
|
<select id="hardware_mapping" name="hardware_mapping" class="form-control">
|
||||||
<option value="adafruit-hat-pwm" {% if main_config.display.hardware.hardware_mapping == "adafruit-hat-pwm" %}selected{% endif %}>Adafruit HAT PWM</option>
|
<option value="adafruit-hat-pwm" {% if main_config.display.hardware.hardware_mapping == "adafruit-hat-pwm" %}selected{% endif %}>Adafruit HAT PWM</option>
|
||||||
<option value="adafruit-hat" {% if main_config.display.hardware.hardware_mapping == "adafruit-hat" %}selected{% endif %}>Adafruit HAT</option>
|
<option value="adafruit-hat" {% if main_config.display.hardware.hardware_mapping == "adafruit-hat" %}selected{% endif %}>Adafruit HAT</option>
|
||||||
@@ -126,7 +124,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" id="setting-display-led_rgb_sequence" data-setting-key="display.hardware.led_rgb_sequence">
|
<div class="form-group" id="setting-display-led_rgb_sequence" data-setting-key="display.hardware.led_rgb_sequence">
|
||||||
<label for="led_rgb_sequence" class="block text-sm font-medium text-gray-700">LED RGB Sequence{{ ui.help_tip('Order the panel expects color channels in.\nChange this only if reds/greens/blues look swapped. Default: RGB.', 'LED RGB Sequence') }}</label>
|
<label for="led_rgb_sequence" class="block text-sm font-medium text-gray-700">LED RGB Sequence{{ ui.help_tip('Order the panel expects color channels in.\nChange this only if reds/greens/blues look swapped (red shows as blue: try BGR). Default: RGB. The Waveshare 96x48 V2 needs BGR.', 'LED RGB Sequence') }}</label>
|
||||||
<select id="led_rgb_sequence" name="led_rgb_sequence" class="form-control">
|
<select id="led_rgb_sequence" name="led_rgb_sequence" class="form-control">
|
||||||
<option value="RGB" {% if main_config.display.hardware.get('led_rgb_sequence', 'RGB') == "RGB" %}selected{% endif %}>RGB</option>
|
<option value="RGB" {% if main_config.display.hardware.get('led_rgb_sequence', 'RGB') == "RGB" %}selected{% endif %}>RGB</option>
|
||||||
<option value="RBG" {% if main_config.display.hardware.get('led_rgb_sequence', 'RGB') == "RBG" %}selected{% endif %}>RBG</option>
|
<option value="RBG" {% if main_config.display.hardware.get('led_rgb_sequence', 'RGB') == "RBG" %}selected{% endif %}>RBG</option>
|
||||||
@@ -160,7 +158,7 @@
|
|||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<div class="form-group" id="setting-display-multiplexing" data-setting-key="display.hardware.multiplexing">
|
<div class="form-group" id="setting-display-multiplexing" data-setting-key="display.hardware.multiplexing">
|
||||||
<label for="multiplexing" class="block text-sm font-medium text-gray-700">Multiplexing{{ ui.help_tip('Pixel-mapping scheme used by outdoor/specialty panels.\nLeave at 0 (Direct) for most indoor panels. Only change if the image is scrambled — try values until it looks right.', 'Multiplexing') }}</label>
|
<label for="multiplexing" class="block text-sm font-medium text-gray-700">Multiplexing{{ ui.help_tip('How the pixels are wired on outdoor/specialty panels (P10, P8, P4 and P3 outdoor modules and similar), whose LEDs are not laid out in straight rows.\nLeave at 0 (Direct) for most indoor panels. If the image is scrambled in a repeating pattern, try the value named after your panel first, then the others.', 'Multiplexing') }}</label>
|
||||||
<select id="multiplexing" name="multiplexing" class="form-control">
|
<select id="multiplexing" name="multiplexing" class="form-control">
|
||||||
<option value="0" {% if main_config.display.hardware.get('multiplexing', 0)|int == 0 %}selected{% endif %}>0 - Direct</option>
|
<option value="0" {% if main_config.display.hardware.get('multiplexing', 0)|int == 0 %}selected{% endif %}>0 - Direct</option>
|
||||||
<option value="1" {% if main_config.display.hardware.get('multiplexing', 0)|int == 1 %}selected{% endif %}>1 - Stripe</option>
|
<option value="1" {% if main_config.display.hardware.get('multiplexing', 0)|int == 1 %}selected{% endif %}>1 - Stripe</option>
|
||||||
@@ -189,7 +187,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" id="setting-display-panel_type" data-setting-key="display.hardware.panel_type">
|
<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">
|
<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="" {% 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>
|
<option value="FM6126A" {% if main_config.display.hardware.get('panel_type', '') == "FM6126A" %}selected{% endif %}>FM6126A</option>
|
||||||
@@ -198,24 +196,25 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" id="setting-display-row_address_type" data-setting-key="display.hardware.row_address_type">
|
<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">
|
<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="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="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="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="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="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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<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">
|
<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 GPIO writes so the panel electronics keep up (0–10); higher is more reliable but lowers the refresh rate.\nStarting points: Pi Zero / Pi 1 → 0–1, Pi 2 / Pi 3 → 1–3, 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. Raise it if the display shows garbage, jumping rows or flicker; in RIO mode higher values may improve performance.', 'GPIO Slowdown') }}</label>
|
||||||
<input type="number"
|
<input type="number"
|
||||||
id="gpio_slowdown"
|
id="gpio_slowdown"
|
||||||
name="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"
|
min="0"
|
||||||
max="10"
|
max="10"
|
||||||
class="form-control">
|
class="form-control">
|
||||||
@@ -223,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 — PIO (default, low CPU)</option>
|
<option value="0" {% if main_config.display.get('runtime', {}).get('rp1_rio', 0)|int == 0 %}selected{% endif %}>0 — PIO (default, low CPU)</option>
|
||||||
@@ -232,7 +231,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" id="setting-display-scan_mode" data-setting-key="display.hardware.scan_mode">
|
<div class="form-group" id="setting-display-scan_mode" data-setting-key="display.hardware.scan_mode">
|
||||||
<label for="scan_mode" class="block text-sm font-medium text-gray-700">Scan Mode{{ ui.help_tip('Order rows are refreshed in.\n0 = progressive (default), 1 = interlaced. Change only if you see banding or flicker on certain panels.', 'Scan Mode') }}</label>
|
<label for="scan_mode" class="block text-sm font-medium text-gray-700">Scan Mode{{ ui.help_tip('Order rows are refreshed in.\n0 = progressive (default), 1 = interlaced. Interlaced can look a little smoother when the refresh rate is very low, but usually shows a comb effect on anything moving. Leave at 0 unless you are tuning a slow setup.', 'Scan Mode') }}</label>
|
||||||
<input type="number"
|
<input type="number"
|
||||||
id="scan_mode"
|
id="scan_mode"
|
||||||
name="scan_mode"
|
name="scan_mode"
|
||||||
@@ -245,7 +244,7 @@
|
|||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div class="form-group" id="setting-display-pwm_bits" data-setting-key="display.hardware.pwm_bits">
|
<div class="form-group" id="setting-display-pwm_bits" data-setting-key="display.hardware.pwm_bits">
|
||||||
<label for="pwm_bits" class="block text-sm font-medium text-gray-700">PWM Bits{{ ui.help_tip('Color depth per channel (1–11).\nHigher means smoother color but a lower refresh rate; lower means faster refresh with more banding. Default: 11 (this build defaults to 9).', 'PWM Bits') }}</label>
|
<label for="pwm_bits" class="block text-sm font-medium text-gray-700">PWM Bits{{ ui.help_tip('Color depth per channel (1–11).\nHigher means smoother color but a lower refresh rate; lower drops the subtlest shades for a faster refresh (1 = 8 colors). Default: 9.', 'PWM Bits') }}</label>
|
||||||
<input type="number"
|
<input type="number"
|
||||||
id="pwm_bits"
|
id="pwm_bits"
|
||||||
name="pwm_bits"
|
name="pwm_bits"
|
||||||
@@ -256,36 +255,36 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" id="setting-display-pwm_dither_bits" data-setting-key="display.hardware.pwm_dither_bits">
|
<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-dithers the lowest color bits (0–2): their brightness comes from showing them on only some frames, which raises the refresh rate.\nDefault: 1. 0 gives the steadiest dim colors; 2 is fastest but dark shades can shimmer. The rgbmatrix library accepts only 0–2.', 'PWM Dither Bits') }}</label>
|
||||||
<input type="number"
|
<input type="number"
|
||||||
id="pwm_dither_bits"
|
id="pwm_dither_bits"
|
||||||
name="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"
|
min="0"
|
||||||
max="4"
|
max="2"
|
||||||
class="form-control">
|
class="form-control">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<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">
|
<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('On-time of the least-significant color bit (50–3000 ns); each higher bit doubles it.\nDefault: 130. Lower allows a higher refresh rate but can cost color accuracy or add ghosting; raise it if bright text on black leaves faint trails.', 'PWM LSB Nanoseconds') }}</label>
|
||||||
<input type="number"
|
<input type="number"
|
||||||
id="pwm_lsb_nanoseconds"
|
id="pwm_lsb_nanoseconds"
|
||||||
name="pwm_lsb_nanoseconds"
|
name="pwm_lsb_nanoseconds"
|
||||||
value="{{ main_config.display.hardware.pwm_lsb_nanoseconds or 130 }}"
|
value="{{ main_config.display.hardware.pwm_lsb_nanoseconds or 130 }}"
|
||||||
min="50"
|
min="50"
|
||||||
max="500"
|
max="3000"
|
||||||
class="form-control">
|
class="form-control">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" id="setting-display-limit_refresh_rate_hz" data-setting-key="display.hardware.limit_refresh_rate_hz">
|
<div class="form-group" id="setting-display-limit_refresh_rate_hz" data-setting-key="display.hardware.limit_refresh_rate_hz">
|
||||||
<label for="limit_refresh_rate_hz" class="block text-sm font-medium text-gray-700">Limit Refresh Rate (Hz){{ ui.help_tip('Caps the panel refresh rate (1–1000 Hz).\nDefault: 120. A steady cap reduces flicker in camera recordings and keeps timing consistent. Set higher or to the max your panel supports for the smoothest motion.', 'Limit Refresh Rate') }}</label>
|
<label for="limit_refresh_rate_hz" class="block text-sm font-medium text-gray-700">Limit Refresh Rate (Hz){{ ui.help_tip('Caps the panel refresh rate (0–1000 Hz; 0 = no cap).\nDefault: 100. A steady cap reduces flicker from other activity on the Pi and in camera recordings. Scroll speeds are worked out against this value (against 100 Hz when it is 0), so a cap the panel can actually hold keeps scrolling even.', 'Limit Refresh Rate') }}</label>
|
||||||
<input type="number"
|
<input type="number"
|
||||||
id="limit_refresh_rate_hz"
|
id="limit_refresh_rate_hz"
|
||||||
name="limit_refresh_rate_hz"
|
name="limit_refresh_rate_hz"
|
||||||
value="{{ main_config.display.hardware.limit_refresh_rate_hz or 120 }}"
|
value="{{ main_config.display.hardware.get('limit_refresh_rate_hz', 100) }}"
|
||||||
min="1"
|
min="0"
|
||||||
max="1000"
|
max="1000"
|
||||||
class="form-control">
|
class="form-control">
|
||||||
</div>
|
</div>
|
||||||
@@ -304,7 +303,7 @@
|
|||||||
{% if main_config.display.hardware.disable_hardware_pulsing %}checked{% endif %}
|
{% if main_config.display.hardware.disable_hardware_pulsing %}checked{% endif %}
|
||||||
class="form-control h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
|
class="form-control h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
|
||||||
<span class="ml-2 text-sm font-medium text-gray-900">Disable Hardware Pulsing</span>
|
<span class="ml-2 text-sm font-medium text-gray-900">Disable Hardware Pulsing</span>
|
||||||
{{ ui.help_tip('Turn off hardware PWM pulsing.\nEnable this if the Pi audio is in use or you hear buzzing / see instability. Slightly increases CPU usage.', 'Disable Hardware Pulsing') }}
|
{{ ui.help_tip('Time the brightness pulses in software instead of with the Pi hardware PWM.\nLeave unchecked where possible: software timing is less exact, so a row or the whole panel can briefly flash brighter. Hardware pulsing needs the panel OE line on GPIO 18 (Adafruit HAT PWM, Regular, Triple Bonnet) and the Pi onboard sound driver disabled; check this if you need Pi audio. Without GPIO 18 the library uses software timing anyway.', 'Disable Hardware Pulsing') }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -328,7 +327,7 @@
|
|||||||
{% if main_config.display.hardware.show_refresh_rate %}checked{% endif %}
|
{% if main_config.display.hardware.show_refresh_rate %}checked{% endif %}
|
||||||
class="form-control h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
|
class="form-control h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
|
||||||
<span class="ml-2 text-sm font-medium text-gray-900">Show Refresh Rate</span>
|
<span class="ml-2 text-sm font-medium text-gray-900">Show Refresh Rate</span>
|
||||||
{{ ui.help_tip('Overlay the live panel refresh rate on the display.\nUseful for tuning GPIO Slowdown and PWM settings; turn off for normal use.', 'Show Refresh Rate') }}
|
{{ ui.help_tip('Print the live panel refresh rate to the console; nothing is drawn on the panel.\nReadable when you stop the service and run the display in a terminal; under the service the output is buffered. Turn off for normal use.', 'Show Refresh Rate') }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user