Source code for pitop.pulse.ledmatrix

from pitop.pulse import configuration

from pitopcommon.logger import PTLogger

from copy import deepcopy
from math import (
    ceil,
    radians,
    sin,
    cos,
)
from os import path
from serial import (
    serialutil,
    Serial
)
import signal
from sys import exit
from time import sleep
from threading import Timer


_initialised = False

_w = 7
_h = 7
_rotation = 0
_brightness = 1.0

_max_freq = 50  # Maximum update speed is 50 times per second
_update_rate = 0.1

_running = False
_show_enabled = True

_gamma_correction_arr = [
    0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1,
    1, 2, 2, 2, 2, 2, 2, 2,
    2, 3, 3, 3, 3, 3, 3, 3,
    4, 4, 4, 4, 4, 5, 5, 5,
    5, 6, 6, 6, 6, 7, 7, 7,
    7, 8, 8, 8, 9, 9, 9, 10,
    10, 10, 11, 11, 11, 12, 12, 13,
    13, 13, 14, 14, 15, 15, 16, 16,
    17, 17, 18, 18, 19, 19, 20, 20,
    21, 21, 22, 22, 23, 24, 24, 25,
    25, 26, 27, 27, 28, 29, 29, 30,
    31, 32, 32, 33, 34, 35, 35, 36,
    37, 38, 39, 39, 40, 41, 42, 43,
    44, 45, 46, 47, 48, 49, 50, 50,
    51, 52, 54, 55, 56, 57, 58, 59,
    60, 61, 62, 63, 64, 66, 67, 68,
    69, 70, 72, 73, 74, 75, 77, 78,
    79, 81, 82, 83, 85, 86, 87, 89,
    90, 92, 93, 95, 96, 98, 99, 101,
    102, 104, 105, 107, 109, 110, 112, 114,
    115, 117, 119, 120, 122, 124, 126, 127,
    129, 131, 133, 135, 137, 138, 140, 142,
    144, 146, 148, 150, 152, 154, 156, 158,
    160, 162, 164, 167, 169, 171, 173, 175,
    177, 180, 182, 184, 186, 189, 191, 193,
    196, 198, 200, 203, 205, 208, 210, 213,
    215, 218, 220, 223, 225, 228, 231, 233,
    236, 239, 241, 244, 247, 249, 252, 255
]

_sync = bytearray(
    [
        7,
        127,
        127,
        127,
        127,
        127,
        127,
        127,
        127,
        127,
        127,
        127,
        127,
        127,
        127,
        127,
        127
    ]
)

_empty = [0, 0, 0]

_empty_map = [
    [_empty, _empty, _empty, _empty, _empty, _empty, _empty],
    [_empty, _empty, _empty, _empty, _empty, _empty, _empty],
    [_empty, _empty, _empty, _empty, _empty, _empty, _empty],
    [_empty, _empty, _empty, _empty, _empty, _empty, _empty],
    [_empty, _empty, _empty, _empty, _empty, _empty, _empty],
    [_empty, _empty, _empty, _empty, _empty, _empty, _empty],
    [_empty, _empty, _empty, _empty, _empty, _empty, _empty]
]

_pixel_map = deepcopy(_empty_map)

#######################
# INTERNAL OPERATIONS #
#######################


def __initialise():
    """INTERNAL.

    Initialise the matrix.
    """

    global _initialised
    global _serial_device
    global _pixel_map

    if not _initialised:
        if configuration.mcu_enabled():
            if not path.exists('/dev/serial0'):
                err_str = "Could not find serial port - are you sure it's enabled?"
                raise serialutil.SerialException(err_str)

            PTLogger.debug("Opening serial port...")

            _serial_device = Serial("/dev/serial0", baudrate=250000, timeout=2)

            if _serial_device.isOpen():
                PTLogger.debug("OK.")
            else:
                PTLogger.info("Error: Failed to open serial port!")
                exit()

            _initialised = True
        else:
            PTLogger.error("Error: pi-topPULSE not initialised by pt-device-manager")


def __signal_handler(signal, frame):
    """INTERNAL.

    Handles signals from the OS to exit.
    """

    PTLogger.info("\nQuitting...")

    stop()
    off()
    exit(0)


def __get_avg_color():
    """INTERNAL.

    Get the average color of the matrix.
    """

    total_rgb = [0, 0, 0]
    avg_rgb = [0, 0, 0]

    for x in range(_w):
        for y in range(_h):
            for c in range(3):
                total_rgb[c] = total_rgb[c] + _pixel_map[x][y][c]

    for i, val in enumerate(total_rgb):
        avg_rgb[i] = int(round(val / (_w * _h)))

    return avg_rgb


def __write(data):
    """INTERNAL.

    Write data to the matrix.
    """

    PTLogger.debug('{s0:<4}{s1:<4}{s2:<4}{s3:<4}{s4:<4}{s5:<4}{s6:<4}{s7:<4}{s8:<4}{s9:<4}{s10:<4}'.format(
        s0=data[0], s1=data[1], s2=data[2], s3=data[3], s4=data[4], s5=data[5], s6=data[6], s7=data[7], s8=data[8], s9=data[9], s10=data[10]))
    _serial_device.write(data)
    sleep(0.002)


def __get_gamma_corrected_value(original_value):
    """INTERNAL.

    Converts a brightness value from 0-255 to the value that produces an
    approximately linear scaling to the human eye.
    """

    return _gamma_correction_arr[original_value]


def __scale_pixel_to_brightness(original_value):
    """INTERNAL.

    Multiplies intended brightness of a pixel by brightness scaling
    factor to generate an adjusted value.
    """

    unrounded_new_brightness = original_value * _brightness
    rounded_new_brightness = round(unrounded_new_brightness)
    int_new_brightness = int(rounded_new_brightness)

    return int_new_brightness


def __get_rotated_pixel_map():
    """INTERNAL.

    Get a rotated copy of the current in-memory pixel map.
    """

    rotated_pixel_map = deepcopy(_pixel_map)

    # Some fancy maths to rotate pixel map so that
    # 0,0 (x,y) - with rotation 0 - is the bottom left LED
    scaled_rotation = int(_rotation / 90)
    adjusted_scaled_rotation = (scaled_rotation + 1)
    modulo_adjusted_scaled_rotation = (adjusted_scaled_rotation % 4)
    count = (6 - modulo_adjusted_scaled_rotation) % 4

    for x in range(count):
        rotated_pixel_map = list(zip(*rotated_pixel_map[::-1]))

    return rotated_pixel_map


def __brightness_correct(original_value):
    """INTERNAL.

    Correct a single color for brightness.
    """

    brightness_scaled = __scale_pixel_to_brightness(original_value)
    new_value = __get_gamma_corrected_value(brightness_scaled)

    return new_value


def __adjust_r_g_b_for_brightness_correction(r, g, b):
    """INTERNAL.

    Correct LED for brightness.
    """

    r = __brightness_correct(r)
    g = __brightness_correct(g)
    b = __brightness_correct(b)

    return r, g, b


def __sync_with_device():
    """INTERNAL.

    Send the sync frame to tell the device that LED data is expected.
    """

    __initialise()
    PTLogger.debug("Sync data:")
    __write(_sync)


def __rgb_to_bytes_to_send(rgb):
    """INTERNAL.

    Format the LED data in the device-specific layout.
    """

    # Create three 5-bit color vals, splitting the green bits
    # into two parts (hardware spec):
    # |XX|G0|G1|R0|R1|R2|R3|R4|
    # |G2|G3|G4|B0|B1|B2|B3|B4|

    r = rgb[0]
    g = rgb[1]
    b = rgb[2]

    byte0 = (r >> 3) & 0x1F
    byte1 = (b >> 3) & 0x1F
    grnb0 = (g >> 1) & 0x60
    grnb1 = (g << 2) & 0xE0

    byte0 = (byte0 | grnb0) & 0xFF
    byte1 = (byte1 | grnb1) & 0xFF

    return byte0, byte1


def __timer_method():
    """INTERNAL.

    Run by the timer on each tick.
    """

    global _running
    global _update_rate

    while _running:
        show()
        sleep(_update_rate)


def __flip(direction):
    """INTERNAL.

    Flip the pixel map.
    """

    global _pixel_map

    flipped_pixel_map = deepcopy(_pixel_map)
    for x in range(_w):
        for y in range(_h):
            if direction == "h":
                flipped_pixel_map[x][y] = _pixel_map[(_w - 1) - x][y]
            elif direction == "v":
                flipped_pixel_map[x][y] = _pixel_map[x][(_h - 1) - y]
            else:
                err = 'Flip direction must be [h]orizontal or [v]ertical only'
                raise ValueError(err)

    _pixel_map = flipped_pixel_map


def __set_show_state(enabled):
    """INTERNAL."""

    global _show_enabled

    _show_enabled = enabled

    if not _show_enabled:
        _temp_disable_t.start()


def __enable_show_state():
    """INTERNAL."""

    __set_show_state(True)


def __disable_show_state():
    """INTERNAL."""

    __set_show_state(True)


#######################
# EXTERNAL OPERATIONS #
#######################

[docs]def set_debug_print_state(debug_enable): """Enable/disable debug prints.""" global _debug _debug = debug_enable
[docs]def brightness(new_brightness): """Set the display brightness between 0.0 and 1.0. :param new_brightness: Brightness from 0.0 to 1.0 (default 1.0) """ global _brightness if new_brightness > 1 or new_brightness < 0: raise ValueError('Brightness level must be between 0 and 1') _brightness = new_brightness
[docs]def get_brightness(): """Get the display brightness value. Returns a float between 0.0 and 1.0. """ return _brightness
[docs]def rotation(new_rotation=0): """Set the display rotation. :param new_rotation: Specify the rotation in degrees: 0, 90, 180 or 270 """ global _rotation if new_rotation in [0, 90, 180, 270]: _rotation = new_rotation return True else: raise ValueError('Rotation: 0, 90, 180 or 270 degrees only')
[docs]def flip_h(): """Flips the grid horizontally.""" __flip("h")
[docs]def flip_v(): """Flips the grid vertically.""" __flip("v")
[docs]def get_shape(): """Returns the shape (width, height) of the display.""" return (_w, _h)
[docs]def get_pixel(x, y): """Get the RGB value of a single pixel. :param x: Horizontal position from 0 to 7 :param y: Veritcal position from 0 to 7 """ global _pixel_map return _pixel_map[y][x]
[docs]def set_pixel(x, y, r, g, b): """Set a single pixel to RGB color. :param x: Horizontal position from 0 to 7 :param y: Veritcal position from 0 to 7 :param r: Amount of red from 0 to 255 :param g: Amount of green from 0 to 255 :param b: Amount of blue from 0 to 255 """ global _pixel_map new_r, new_g, new_b = __adjust_r_g_b_for_brightness_correction(r, g, b) _pixel_map[y][x] = [new_r, new_g, new_b]
[docs]def set_all(r, g, b): """Set all pixels to a specific color.""" global _pixel_map for x in range(_w): for y in range(_h): new_r, new_g, new_b = __adjust_r_g_b_for_brightness_correction( r, g, b) _pixel_map[x][y][0] = new_r _pixel_map[x][y][1] = new_g _pixel_map[x][y][2] = new_b
[docs]def show(): """Update pi-topPULSE with the contents of the display buffer.""" global _pixel_map global _rotation global _show_enabled wait_counter = 0 attempt_to_show_early = not _show_enabled if attempt_to_show_early: PTLogger.info( "Can't update pi-topPULSE LEDs more than 50/s. Waiting...") pause_length = 0.001 # Scale wait time to _max_freq wait_counter_length = ceil(float(1 / float(_max_freq * pause_length))) while not _show_enabled: if wait_counter >= wait_counter_length: # Timer hasn't reset for some reason - force override __enable_show_state() break else: sleep(pause_length) wait_counter = wait_counter + 1 if attempt_to_show_early: PTLogger.debug("pi-topPULSE LEDs re-enabled.") __sync_with_device() rotated_pixel_map = __get_rotated_pixel_map() avg_rgb = __get_avg_color() __initialise() PTLogger.debug("LED data:") # For each col for x in range(_w): # Write col to LED matrix # Start with col no., so LED matrix knows which one it belongs to pixel_map_buffer = chr(x) # Get col's frame buffer, iterating over each pixel for y in range(_h + 1): if y == _h: # Ambient lighting bytes byte0, byte1 = __rgb_to_bytes_to_send(avg_rgb) else: byte0, byte1 = __rgb_to_bytes_to_send(rotated_pixel_map[x][y]) pixel_map_buffer += chr(byte0) pixel_map_buffer += chr(byte1) # Write col to LED matrix arr = bytearray(pixel_map_buffer, 'Latin_1') __write(arr) # Prevent another write if it's too fast __disable_show_state()
[docs]def clear(): """Clear the buffer.""" global _pixel_map _pixel_map = deepcopy(_empty_map)
[docs]def off(): """Clear the buffer and immediately update pi-topPULSE.""" clear() show()
[docs]def run_tests(): """Runs a series of tests to check the LED board is working as expected.""" off() # ------------------------------ # Pixels # ------------------------------ counter = 0 for r in range(4): rotation(90 * r) for x in range(_w): for y in range(_h): rad = radians((float(counter) / (4 * _w * _h)) * 360) r = int((sin(rad) * 127) + 127) g = int((cos(rad) * 127) + 127) b = 255 - int((sin(rad) * 127) + 127) set_pixel(x, y, r, g, b) show() sleep(0.05) counter = counter + 1 off() sleep(0.2) # ------------------------------ # Rows and rotation # ------------------------------ for r in range(4): rotation(90 * r) for c in range(3): for x in range(_w): for y in range(_h): set_pixel(x, y, 255 if c == 0 else 0, 255 if c == 1 else 0, 255 if c == 2 else 0) show() sleep(0.05) off() sleep(0.2) # ------------------------------ # Brightness # ------------------------------ for b in range(100): brightness(float(b) / 100) set_all(255, 255, 255) show() sleep(0.01) for b in range(100): brightness(1 - (float(b) / 100)) set_all(255, 255, 255) show() sleep(0.01) off() brightness(1.0) sleep(0.2) # ------------------------------ # Flipping # ------------------------------ for x in range(int(_w / 2)): for y in range(int(_h / 2)): set_pixel(x, y, 255, 255, 255) set_pixel(int(_w / 4), int(_h / 4), 0, 255, 0) show() sleep(0.5) for f in range(4): for x in range(2): if x == 0: flip_h() else: flip_v() show() sleep(0.5) off() sleep(0.2) # ------------------------------ # Conway - auto refresh # ------------------------------ start(0.1) life_map = [[0, 0, 0, 0, 0, 0, 0], [0, 1, 0, 0, 0, 0, 0], [0, 0, 1, 0, 0, 0, 0], [1, 1, 1, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0]] for r in range(40): temp_map = deepcopy(life_map) for x in range(_w): for y in range(_h): current_cell = temp_map[x][y] neighbours = 0 neighbours = neighbours + temp_map[(x - 1) % _w][(y - 1) % _h] neighbours = neighbours + temp_map[(x - 1) % _w][(y - 0) % _h] neighbours = neighbours + temp_map[(x - 1) % _w][(y + 1) % _h] neighbours = neighbours + temp_map[(x - 0) % _w][(y - 1) % _h] neighbours = neighbours + temp_map[(x - 0) % _w][(y + 1) % _h] neighbours = neighbours + temp_map[(x + 1) % _w][(y - 1) % _h] neighbours = neighbours + temp_map[(x + 1) % _w][(y - 0) % _h] neighbours = neighbours + temp_map[(x + 1) % _w][(y + 1) % _h] if current_cell == 1 and (neighbours < 2 or neighbours > 3): life_map[x][y] = 0 if (current_cell == 0 and neighbours == 3): life_map[x][y] = 1 for x in range(_w): for y in range(_h): if (life_map[x][y] == 1): set_pixel(x, y, 255, 255, 0) else: set_pixel(x, y, 0, 128, 0) sleep(0.1) stop() off()
[docs]def start(new_update_rate=0.1): """Starts a timer to automatically refresh the LEDs.""" global _update_rate global _running global _auto_refresh_timer if new_update_rate < (1 / _max_freq): _update_rate = (1 / _max_freq) else: _update_rate = new_update_rate _running = True _auto_refresh_timer.start()
[docs]def stop(): """Stops the timer that automatically refreshes the LEDs.""" global _running global _auto_refresh_timer _running = False _auto_refresh_timer.cancel()
################## # INITIALISATION # ################## _signal = signal.signal(signal.SIGINT, __signal_handler) _auto_refresh_timer = Timer(_update_rate, __timer_method) _temp_disable_t = Timer(_max_freq, __enable_show_state) clear()