137 lines
4.6 KiB
Python
137 lines
4.6 KiB
Python
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from app.display.matrix_driver import MatrixDriverOptions, validate_driver_options
|
|
from app.display.native_model import (
|
|
HEIGHT,
|
|
HARDWARE_MAPPING,
|
|
HUB75_PINS,
|
|
PI_BANK_OFFSET,
|
|
PI_DATA_OFFSET,
|
|
PIO_BASE,
|
|
PWM_BASE,
|
|
PWM_BITS,
|
|
REFRESH_RATE_LIMITS,
|
|
RESERVED_PI_BITS,
|
|
SCAN_ROWS,
|
|
WIDTH,
|
|
address_mask,
|
|
bitplane_index,
|
|
build_bitplanes,
|
|
map_channel,
|
|
oe_ticks_for_plane,
|
|
)
|
|
|
|
|
|
def test_walnutpi_pin_mapping_matches_fixed_hub75_contract():
|
|
assert [pin.signal for pin in HUB75_PINS] == [
|
|
"R1", "G1", "B1", "R2", "G2", "B2", "A", "B", "C", "D", "E", "CLK", "OE", "LAT",
|
|
]
|
|
assert [pin.physical for pin in HUB75_PINS] == [29, 31, 33, 35, 37, 8, 10, 28, 27, 15, 16, 38, 40, 36]
|
|
assert [pin.bit for pin in HUB75_PINS] == [0, 1, 2, 3, 4, 5, 6, 9, 10, 11, 12, 13, 14, 15]
|
|
assert not ({pin.bit for pin in HUB75_PINS} & RESERVED_PI_BITS)
|
|
assert RESERVED_PI_BITS == {7, 8, 16}
|
|
assert HUB75_PINS[-2].gpio == "PI14/PWM4"
|
|
assert HUB75_PINS[-1].gpio == "PI15"
|
|
assert HARDWARE_MAPPING == "walnutpi-pi-bank-pwm-oe-v2"
|
|
|
|
|
|
def test_h618_pi_register_addresses_are_fixed():
|
|
assert PIO_BASE == 0x0300B000
|
|
assert PWM_BASE == 0x0300A000
|
|
assert PI_BANK_OFFSET == 0x120
|
|
assert PI_DATA_OFFSET == 0x10
|
|
assert PIO_BASE + PI_BANK_OFFSET + PI_DATA_OFFSET == 0x0300B130
|
|
|
|
|
|
def test_row_address_uses_abcde_bits_in_pi_bank():
|
|
assert address_mask(0) == 0
|
|
assert address_mask(1) == 1 << 6
|
|
assert address_mask(2) == 1 << 9
|
|
assert address_mask(16) == 1 << 12
|
|
assert address_mask(31) == sum(1 << bit for bit in (6, 9, 10, 11, 12))
|
|
with pytest.raises(ValueError):
|
|
address_mask(32)
|
|
|
|
|
|
def test_bitplanes_pair_top_and_bottom_rows_without_address_or_control_bits():
|
|
frame = bytearray(WIDTH * HEIGHT * 3)
|
|
frame[0:3] = bytes((0xFF, 0x80, 0x40))
|
|
bottom = SCAN_ROWS * WIDTH * 3
|
|
frame[bottom:bottom + 3] = bytes((0x20, 0x10, 0x08))
|
|
|
|
planes = build_bitplanes(bytes(frame))
|
|
|
|
expected = [0] * PWM_BITS
|
|
values = (0xFF, 0x80, 0x40, 0x20, 0x10, 0x08)
|
|
for component, value in enumerate(values):
|
|
mapped = map_channel(value, 100) >> 4
|
|
for plane in range(PWM_BITS):
|
|
if mapped & (1 << plane):
|
|
expected[plane] |= 1 << component
|
|
assert [planes[bitplane_index(0, plane, 0)] for plane in range(PWM_BITS)] == expected
|
|
assert all(word & ~0x3F == 0 for word in planes)
|
|
|
|
|
|
def test_white_frame_sets_all_six_color_bits_in_all_seven_planes():
|
|
planes = build_bitplanes(bytes([0xFF]) * (WIDTH * HEIGHT * 3))
|
|
assert len(planes) == SCAN_ROWS * PWM_BITS * WIDTH
|
|
assert set(planes) == {0x3F}
|
|
|
|
|
|
def test_bitplane_model_rejects_wrong_frame_size_and_coordinates():
|
|
with pytest.raises(ValueError, match="64x64"):
|
|
build_bitplanes(b"bad")
|
|
with pytest.raises(ValueError, match="out of range"):
|
|
bitplane_index(0, PWM_BITS, 0)
|
|
|
|
|
|
@pytest.mark.parametrize("refresh_rate", sorted(REFRESH_RATE_LIMITS))
|
|
def test_native_driver_accepts_only_documented_refresh_rates(refresh_rate):
|
|
validate_driver_options(MatrixDriverOptions(limit_refresh_rate_hz=refresh_rate))
|
|
|
|
|
|
def test_native_driver_rejects_geometry_pwm_mapping_and_unknown_rate_changes():
|
|
for options in (
|
|
MatrixDriverOptions(rows=32),
|
|
MatrixDriverOptions(cols=128),
|
|
MatrixDriverOptions(scan_rows=16),
|
|
MatrixDriverOptions(row_address_bits=4),
|
|
MatrixDriverOptions(pwm_bits=6),
|
|
MatrixDriverOptions(hardware_mapping="other"),
|
|
MatrixDriverOptions(limit_refresh_rate_hz=70),
|
|
):
|
|
with pytest.raises(ValueError):
|
|
validate_driver_options(options)
|
|
|
|
|
|
def test_cie_brightness_is_monotonic_and_pwm4_weights_are_fixed():
|
|
mapped = [map_channel(255, brightness) for brightness in range(1, 101)]
|
|
assert mapped == sorted(mapped)
|
|
assert mapped[-1] == 2047
|
|
for brightness in range(1, 101):
|
|
for plane in range(PWM_BITS):
|
|
ticks, remainder = oe_ticks_for_plane(10_250_000, brightness, plane, 999)
|
|
assert ticks == 1 << (plane + 4)
|
|
assert remainder == 0
|
|
|
|
|
|
def test_brightness_changes_bitplanes_instead_of_temporally_dithering_oe():
|
|
white = bytes([0xFF]) * (WIDTH * HEIGHT * 3)
|
|
low = build_bitplanes(white, brightness=20)
|
|
high = build_bitplanes(white, brightness=100)
|
|
assert low != high
|
|
assert set(high) == {0x3F}
|
|
|
|
|
|
@pytest.mark.parametrize("arguments", [
|
|
(0, 1, 0, 0),
|
|
(5_000_000, 0, 0, 0),
|
|
(5_000_000, 1, PWM_BITS, 0),
|
|
(5_000_000, 1, 0, -1),
|
|
])
|
|
def test_pwm4_fixed_point_rejects_invalid_inputs(arguments):
|
|
with pytest.raises(ValueError):
|
|
oe_ticks_for_plane(*arguments)
|