Skip to content

Display

Display

Game Display is a class that abstracts different S^3 panels into one display

Source code in display/display.py
class Display:
    """Game Display is a class that abstracts different S^3 panels into one display"""

    def __init__(self, board_objects, x_width, y_height):
        """Constructor

        board_objects -- 2d array of seven segment objects oriented how the sign is put together, i.e. [[panel0, panel1],[panel2,panel3]]
        x_width -- number of digits in the display on the x axis
        y_height -- number of pixels on the y axis (each vertical digit is split into two pixels)
        """
        self.board_objects = board_objects
        self.x_width = int(x_width)
        self.y_height = int(y_height)
        self.display_buf = [
            [0 for x in range(self.x_width)] for y in range(self.y_height // 2)
        ]
        self.changed_list = []

    def draw_raw(self, x, y, value, push=False):
        """Draw to a specific segment on the screen

        x -- x coordinate
        y -- y coordinate
        value -- which leds to turn on for the segment
        push -- when true all the recent changes are pushed to the display
        """
        self.display_buf[y][x] = value
        self.changed_list.append((x, y))
        if push:
            self.push()

    def get_raw(self, x, y):
        """Get the value of the segment

        x -- x coordinate
        y -- y coordinate
        """
        return self.display_buf[y][x]

    def draw_pixel(self, x, y, value, combine=True, push=False):
        """Draw shape to one pixel location

        x -- x coordinate
        y -- y coordinate
        value -- which leds to turn on for the pixel (1 for bottom, 2 for left, 4 for top, 8 for right, add sides together for multiple sides)
        combine -- digits are split into two pixels, when drawing to one combine keeps what is already drawn on the other side of the digit
        push -- when true all the recent changes are pushed to the display
        """
        half_height = y // 2
        if value == 0 and combine:
            current_value = self.display_buf[half_height][x]
            if y % 2:
                if current_value & 0x62:
                    self.display_buf[half_height][x] = current_value & 0x63
                else:
                    self.display_buf[half_height][x] = current_value & 0x62
            else:
                if current_value & 0x1C:
                    self.display_buf[half_height][x] = current_value & 0x1D
                else:
                    self.display_buf[half_height][x] = current_value & 0x1C
            self.changed_list.append((x, y // 2))
            return

        if y % 2:
            value = (
                (value & 1) << 3
                | (value & 2) << 1
                | (value & 4) >> 2
                | (value & 8) << 1
            )
        else:
            value = (value & 3) | (value & 4) << 4 | (value & 8) << 2
        # if value | self.display_buf[half_height][x] == self.display_buf[half_height][x]:
        #     return
        if combine:
            value = self.display_buf[half_height][x] | value
        self.display_buf[half_height][x] = value
        self.changed_list.append((x, y // 2))
        if push:
            self.push()

    def get_pixel(self, x, y):
        """Get the value already at the pixel

        x -- x coordinate
        y -- y coordinate
        """
        half_height = y // 2
        value = self.display_buf[half_height][x]

        if y % 2:
            half_value = (
                bool(value & 16) << 3
                | bool(value & 1) << 2
                | bool(value & 4) << 1
                | bool(value & 8)
            )
        else:
            half_value = (
                bool(value & 32) << 3
                | bool(value & 64) << 2
                | bool(value & 2) << 1
                | bool(value & 1)
            )

        return half_value

    def push(self):
        """Push all the recent changes to the display"""
        for location in self.changed_list:
            self.board_objects[location[1] // 6][location[0] // 16].raw2(
                location[0] % 16,
                location[1] % 6,
                self.display_buf[location[1]][location[0]],
            )
        for row in self.board_objects:
            for board in row:
                board.flush()
        self.changed_list.clear()

    def clear(self):
        """Clear all the panels on the display"""
        self.display_buf = [
            [0 for x in range(self.x_width)] for y in range(self.y_height // 2)
        ]
        for row in self.board_objects:
            for board in row:
                board.clear()
        self.changed_list.clear()

    def draw_hline(self, start_x, start_y, length, top=True, combine=True, push=False):
        """Draw horizontal line

        start_x -- x coordinate
        start_y -- y coordinate
        length -- length of line to draw
        top -- draw horizontal line on the top or bottom of the pixel
        combine -- digits are split into two pixels, when drawing to one combine keeps what is already drawn on the other side of the digit
        push -- when true all the recent changes are pushed to the display
        """
        half_height = start_y // 2
        if start_y % 2:
            for x in range(length):
                if top:
                    if combine:
                        self.display_buf[half_height][x + start_x] = (
                            self.display_buf[half_height][x + start_x] | 0x01
                        )
                    else:
                        self.display_buf[half_height][x + start_x] = 0x01
                else:
                    if combine:
                        self.display_buf[half_height][x + start_x] = (
                            self.display_buf[half_height][x + start_x] | 0x08
                        )
                    else:
                        self.display_buf[half_height][x + start_x] = 0x08
                self.changed_list.append((x + start_x, half_height))
        else:
            for x in range(length):
                if top:
                    if combine:
                        self.display_buf[half_height][x + start_x] = (
                            self.display_buf[half_height][x + start_x] | 0x40
                        )
                    else:
                        self.display_buf[half_height][x + start_x] = 0x40
                else:
                    if combine:
                        self.display_buf[half_height][x + start_x] = (
                            self.display_buf[half_height][x + start_x] | 0x01
                        )
                    else:
                        self.display_buf[half_height][x + start_x] = 0x01
                self.changed_list.append((x + start_x, half_height))
        if push:
            self.push()

    def draw_vline(self, start_x, start_y, length, left=True, combine=True, push=False):
        """Draw vertical line

        start_x -- x coordinate
        start_y -- y coordinate
        length -- length of line to draw
        left -- draw vertial line on the left or right of the pixel
        combine -- digits are split into two pixels, when drawing to one combine keeps what is already drawn on the other side of the digit
        push -- when true all the recent changes are pushed to the display
        """
        for y in range(length):
            y_loc = start_y + y
            y_half = y_loc // 2
            if y_loc % 2:
                if left:
                    if combine:
                        self.display_buf[y_half][start_x] = (
                            self.display_buf[y_half][start_x] | 0x04
                        )
                    else:
                        self.display_buf[y_half][start_x] = 0x04
                else:
                    if combine:
                        self.display_buf[y_half][start_x] = (
                            self.display_buf[y_half][start_x] | 0x10
                        )
                    else:
                        self.display_buf[y_half][start_x] = 0x10
            else:
                if left:
                    if combine:
                        self.display_buf[y_half][start_x] = (
                            self.display_buf[y_half][start_x] | 0x02
                        )
                    else:
                        self.display_buf[y_half][start_x] = 0x02
                else:
                    if combine:
                        self.display_buf[y_half][start_x] = (
                            self.display_buf[y_half][start_x] | 0x20
                        )
                    else:
                        self.display_buf[y_half][start_x] = 0x20
            self.changed_list.append((start_x, y_half))
        if push:
            self.push()

    def draw_shape_line(
        self, start_x, start_y, end_x, end_y, value, combine=True, push=False
    ):
        """Draw line with given value at each pixel, can be diagonal

        start_x -- starting x coordinate
        start_y -- starting y coordinate
        end_x -- ending x coordinate
        end_y -- ending y coordiante
        value -- which leds to turn on for the pixel (1 for bottom, 2 for left, 4 for top, 8 for right, add sides together for multiple sides)
        combine -- digits are split into two pixels, when drawing to one combine keeps what is already drawn on the other side of the digit
        push -- when true all the recent changes are pushed to the display
        """
        if start_x != end_x:
            slope = (end_y - start_y) / (end_x - start_x)
            b = start_y - slope * start_x
            for x in range(min(start_x, end_x), max(start_x, end_x) + 1):
                self.draw_pixel(x, round(b) + round(slope * x), value, combine)
        else:
            for y in range(min(start_y, end_y), max(start_y, end_y) + 1):
                self.draw_pixel(start_x, y, value, combine)
        if push:
            self.push()

    def draw_text(self, x, y, msg, combine=True, push=False):
        """Print a message to the screen, y_height-2 is lowest y value accepted without error

        x -- x coordinate
        y -- y coordinate
        msg -- string message to print
        combine -- digits are split into two pixels, when drawing to one combine keeps what is already drawn on the other side of the digit
        push -- when true all the recent changes are pushed to the display
        """
        if y % 2:
            for pos, char in enumerate(msg):
                value = symbols.get_char2(char)
                # get top and bottom values out of value
                top_value = (value & 0x20) >> 2 | (value & 0x40) >> 4 | (value & 0x02)
                bottom_value = (
                    (value & 0x04) >> 1
                    | (value & 0x08) >> 3
                    | (value & 0x10) >> 1
                    | (value & 0x01) << 2
                )
                self.draw_pixel(x + pos, y, top_value, combine=combine)
                self.draw_pixel(x + pos, y + 1, bottom_value, combine=combine)
        else:
            half_y = y // 2
            for pos, char in enumerate(msg):
                value = symbols.get_char2(char)
                self.display_buf[half_y][x + pos] = value
                self.changed_list.append((x + pos, half_y))
        if push:
            self.push()

    # work in progress
    def fill_box(self, start_x, start_y, x_len, y_len, push=False):
        if push:
            self.push()

__init__(board_objects, x_width, y_height)

Constructor

board_objects -- 2d array of seven segment objects oriented how the sign is put together, i.e. [[panel0, panel1],[panel2,panel3]] x_width -- number of digits in the display on the x axis y_height -- number of pixels on the y axis (each vertical digit is split into two pixels)

Source code in display/display.py
def __init__(self, board_objects, x_width, y_height):
    """Constructor

    board_objects -- 2d array of seven segment objects oriented how the sign is put together, i.e. [[panel0, panel1],[panel2,panel3]]
    x_width -- number of digits in the display on the x axis
    y_height -- number of pixels on the y axis (each vertical digit is split into two pixels)
    """
    self.board_objects = board_objects
    self.x_width = int(x_width)
    self.y_height = int(y_height)
    self.display_buf = [
        [0 for x in range(self.x_width)] for y in range(self.y_height // 2)
    ]
    self.changed_list = []

clear()

Clear all the panels on the display

Source code in display/display.py
def clear(self):
    """Clear all the panels on the display"""
    self.display_buf = [
        [0 for x in range(self.x_width)] for y in range(self.y_height // 2)
    ]
    for row in self.board_objects:
        for board in row:
            board.clear()
    self.changed_list.clear()

draw_hline(start_x, start_y, length, top=True, combine=True, push=False)

Draw horizontal line

start_x -- x coordinate start_y -- y coordinate length -- length of line to draw top -- draw horizontal line on the top or bottom of the pixel combine -- digits are split into two pixels, when drawing to one combine keeps what is already drawn on the other side of the digit push -- when true all the recent changes are pushed to the display

Source code in display/display.py
def draw_hline(self, start_x, start_y, length, top=True, combine=True, push=False):
    """Draw horizontal line

    start_x -- x coordinate
    start_y -- y coordinate
    length -- length of line to draw
    top -- draw horizontal line on the top or bottom of the pixel
    combine -- digits are split into two pixels, when drawing to one combine keeps what is already drawn on the other side of the digit
    push -- when true all the recent changes are pushed to the display
    """
    half_height = start_y // 2
    if start_y % 2:
        for x in range(length):
            if top:
                if combine:
                    self.display_buf[half_height][x + start_x] = (
                        self.display_buf[half_height][x + start_x] | 0x01
                    )
                else:
                    self.display_buf[half_height][x + start_x] = 0x01
            else:
                if combine:
                    self.display_buf[half_height][x + start_x] = (
                        self.display_buf[half_height][x + start_x] | 0x08
                    )
                else:
                    self.display_buf[half_height][x + start_x] = 0x08
            self.changed_list.append((x + start_x, half_height))
    else:
        for x in range(length):
            if top:
                if combine:
                    self.display_buf[half_height][x + start_x] = (
                        self.display_buf[half_height][x + start_x] | 0x40
                    )
                else:
                    self.display_buf[half_height][x + start_x] = 0x40
            else:
                if combine:
                    self.display_buf[half_height][x + start_x] = (
                        self.display_buf[half_height][x + start_x] | 0x01
                    )
                else:
                    self.display_buf[half_height][x + start_x] = 0x01
            self.changed_list.append((x + start_x, half_height))
    if push:
        self.push()

draw_pixel(x, y, value, combine=True, push=False)

Draw shape to one pixel location

x -- x coordinate y -- y coordinate value -- which leds to turn on for the pixel (1 for bottom, 2 for left, 4 for top, 8 for right, add sides together for multiple sides) combine -- digits are split into two pixels, when drawing to one combine keeps what is already drawn on the other side of the digit push -- when true all the recent changes are pushed to the display

Source code in display/display.py
def draw_pixel(self, x, y, value, combine=True, push=False):
    """Draw shape to one pixel location

    x -- x coordinate
    y -- y coordinate
    value -- which leds to turn on for the pixel (1 for bottom, 2 for left, 4 for top, 8 for right, add sides together for multiple sides)
    combine -- digits are split into two pixels, when drawing to one combine keeps what is already drawn on the other side of the digit
    push -- when true all the recent changes are pushed to the display
    """
    half_height = y // 2
    if value == 0 and combine:
        current_value = self.display_buf[half_height][x]
        if y % 2:
            if current_value & 0x62:
                self.display_buf[half_height][x] = current_value & 0x63
            else:
                self.display_buf[half_height][x] = current_value & 0x62
        else:
            if current_value & 0x1C:
                self.display_buf[half_height][x] = current_value & 0x1D
            else:
                self.display_buf[half_height][x] = current_value & 0x1C
        self.changed_list.append((x, y // 2))
        return

    if y % 2:
        value = (
            (value & 1) << 3
            | (value & 2) << 1
            | (value & 4) >> 2
            | (value & 8) << 1
        )
    else:
        value = (value & 3) | (value & 4) << 4 | (value & 8) << 2
    # if value | self.display_buf[half_height][x] == self.display_buf[half_height][x]:
    #     return
    if combine:
        value = self.display_buf[half_height][x] | value
    self.display_buf[half_height][x] = value
    self.changed_list.append((x, y // 2))
    if push:
        self.push()

draw_raw(x, y, value, push=False)

Draw to a specific segment on the screen

x -- x coordinate y -- y coordinate value -- which leds to turn on for the segment push -- when true all the recent changes are pushed to the display

Source code in display/display.py
def draw_raw(self, x, y, value, push=False):
    """Draw to a specific segment on the screen

    x -- x coordinate
    y -- y coordinate
    value -- which leds to turn on for the segment
    push -- when true all the recent changes are pushed to the display
    """
    self.display_buf[y][x] = value
    self.changed_list.append((x, y))
    if push:
        self.push()

draw_shape_line(start_x, start_y, end_x, end_y, value, combine=True, push=False)

Draw line with given value at each pixel, can be diagonal

start_x -- starting x coordinate start_y -- starting y coordinate end_x -- ending x coordinate end_y -- ending y coordiante value -- which leds to turn on for the pixel (1 for bottom, 2 for left, 4 for top, 8 for right, add sides together for multiple sides) combine -- digits are split into two pixels, when drawing to one combine keeps what is already drawn on the other side of the digit push -- when true all the recent changes are pushed to the display

Source code in display/display.py
def draw_shape_line(
    self, start_x, start_y, end_x, end_y, value, combine=True, push=False
):
    """Draw line with given value at each pixel, can be diagonal

    start_x -- starting x coordinate
    start_y -- starting y coordinate
    end_x -- ending x coordinate
    end_y -- ending y coordiante
    value -- which leds to turn on for the pixel (1 for bottom, 2 for left, 4 for top, 8 for right, add sides together for multiple sides)
    combine -- digits are split into two pixels, when drawing to one combine keeps what is already drawn on the other side of the digit
    push -- when true all the recent changes are pushed to the display
    """
    if start_x != end_x:
        slope = (end_y - start_y) / (end_x - start_x)
        b = start_y - slope * start_x
        for x in range(min(start_x, end_x), max(start_x, end_x) + 1):
            self.draw_pixel(x, round(b) + round(slope * x), value, combine)
    else:
        for y in range(min(start_y, end_y), max(start_y, end_y) + 1):
            self.draw_pixel(start_x, y, value, combine)
    if push:
        self.push()

draw_text(x, y, msg, combine=True, push=False)

Print a message to the screen, y_height-2 is lowest y value accepted without error

x -- x coordinate y -- y coordinate msg -- string message to print combine -- digits are split into two pixels, when drawing to one combine keeps what is already drawn on the other side of the digit push -- when true all the recent changes are pushed to the display

Source code in display/display.py
def draw_text(self, x, y, msg, combine=True, push=False):
    """Print a message to the screen, y_height-2 is lowest y value accepted without error

    x -- x coordinate
    y -- y coordinate
    msg -- string message to print
    combine -- digits are split into two pixels, when drawing to one combine keeps what is already drawn on the other side of the digit
    push -- when true all the recent changes are pushed to the display
    """
    if y % 2:
        for pos, char in enumerate(msg):
            value = symbols.get_char2(char)
            # get top and bottom values out of value
            top_value = (value & 0x20) >> 2 | (value & 0x40) >> 4 | (value & 0x02)
            bottom_value = (
                (value & 0x04) >> 1
                | (value & 0x08) >> 3
                | (value & 0x10) >> 1
                | (value & 0x01) << 2
            )
            self.draw_pixel(x + pos, y, top_value, combine=combine)
            self.draw_pixel(x + pos, y + 1, bottom_value, combine=combine)
    else:
        half_y = y // 2
        for pos, char in enumerate(msg):
            value = symbols.get_char2(char)
            self.display_buf[half_y][x + pos] = value
            self.changed_list.append((x + pos, half_y))
    if push:
        self.push()

draw_vline(start_x, start_y, length, left=True, combine=True, push=False)

Draw vertical line

start_x -- x coordinate start_y -- y coordinate length -- length of line to draw left -- draw vertial line on the left or right of the pixel combine -- digits are split into two pixels, when drawing to one combine keeps what is already drawn on the other side of the digit push -- when true all the recent changes are pushed to the display

Source code in display/display.py
def draw_vline(self, start_x, start_y, length, left=True, combine=True, push=False):
    """Draw vertical line

    start_x -- x coordinate
    start_y -- y coordinate
    length -- length of line to draw
    left -- draw vertial line on the left or right of the pixel
    combine -- digits are split into two pixels, when drawing to one combine keeps what is already drawn on the other side of the digit
    push -- when true all the recent changes are pushed to the display
    """
    for y in range(length):
        y_loc = start_y + y
        y_half = y_loc // 2
        if y_loc % 2:
            if left:
                if combine:
                    self.display_buf[y_half][start_x] = (
                        self.display_buf[y_half][start_x] | 0x04
                    )
                else:
                    self.display_buf[y_half][start_x] = 0x04
            else:
                if combine:
                    self.display_buf[y_half][start_x] = (
                        self.display_buf[y_half][start_x] | 0x10
                    )
                else:
                    self.display_buf[y_half][start_x] = 0x10
        else:
            if left:
                if combine:
                    self.display_buf[y_half][start_x] = (
                        self.display_buf[y_half][start_x] | 0x02
                    )
                else:
                    self.display_buf[y_half][start_x] = 0x02
            else:
                if combine:
                    self.display_buf[y_half][start_x] = (
                        self.display_buf[y_half][start_x] | 0x20
                    )
                else:
                    self.display_buf[y_half][start_x] = 0x20
        self.changed_list.append((start_x, y_half))
    if push:
        self.push()

get_pixel(x, y)

Get the value already at the pixel

x -- x coordinate y -- y coordinate

Source code in display/display.py
def get_pixel(self, x, y):
    """Get the value already at the pixel

    x -- x coordinate
    y -- y coordinate
    """
    half_height = y // 2
    value = self.display_buf[half_height][x]

    if y % 2:
        half_value = (
            bool(value & 16) << 3
            | bool(value & 1) << 2
            | bool(value & 4) << 1
            | bool(value & 8)
        )
    else:
        half_value = (
            bool(value & 32) << 3
            | bool(value & 64) << 2
            | bool(value & 2) << 1
            | bool(value & 1)
        )

    return half_value

get_raw(x, y)

Get the value of the segment

x -- x coordinate y -- y coordinate

Source code in display/display.py
def get_raw(self, x, y):
    """Get the value of the segment

    x -- x coordinate
    y -- y coordinate
    """
    return self.display_buf[y][x]

push()

Push all the recent changes to the display

Source code in display/display.py
def push(self):
    """Push all the recent changes to the display"""
    for location in self.changed_list:
        self.board_objects[location[1] // 6][location[0] // 16].raw2(
            location[0] % 16,
            location[1] % 6,
            self.display_buf[location[1]][location[0]],
        )
    for row in self.board_objects:
        for board in row:
            board.flush()
    self.changed_list.clear()

SegmentDisplay

SegmentDisplay is a class that abstracts the S^3 screen into individually controllable segments. Instead of making each circle of a segment a pixel, this class lets you control each segment as a pixel. This means this display is 2 times wider (there are two horizontal segments) and 3 times higher (there are three vertical segments) than the Display object.

Source code in display/segment_display.py
class SegmentDisplay:
    """
    SegmentDisplay is a class that abstracts the S^3 screen into individually
    controllable segments. Instead of making each circle of a segment a pixel,
    this class lets you control each segment as a pixel. This means this
    display is 2 times wider (there are two horizontal segments) and 3 times
    higher (there are three vertical segments) than the Display object.
    """

    def __init__(self, screen):
        self.screen = screen

        self.screen_width = screen.x_width - 1
        self.screen_height = screen.y_height // 2

        self.width = 2 * self.screen_width
        self.height = 3 * self.screen_height

        self.x_buffer = np.zeros((self.width, self.height))
        self.y_buffer = np.zeros((self.width, self.height))

    def draw(self, push=True):
        """
        Updates the screen with all of the lines drawn using draw_line.

        push -- when true all the recent changes are pushed to the display
        """
        for x in range(self.screen_width):
            for y in range(self.screen_height):

                state = 0

                if self.x_buffer[x * 2][y * 3]:
                    state += 64  # 64 = TOP
                if self.x_buffer[x * 2][y * 3 + 1]:
                    state += 1  # 1 = CENTER
                if self.x_buffer[x * 2][y * 3 + 2]:
                    state += 8  # 8 = BOTTOM

                if self.y_buffer[x * 2 + 1][y * 3]:
                    state += 32  # 32 = TR
                if self.y_buffer[x * 2 + 1][y * 3 + 1]:
                    state += 16  # 16 = BR
                if self.y_buffer[x * 2][y * 3 + 1]:
                    state += 4  # 4 = BL
                if self.y_buffer[x * 2][y * 3]:
                    state += 2  # 2 = TL

                # Only draw the pixel if there is something to draw
                if state != 0:
                    self.screen.draw_raw(x, y, state)

        if push:
            self.screen.push()

    def undraw(self):
        """
        Clears all segments drawn both from the screen and from the underlying data structure.
        """
        for x in range(self.screen_width):
            for y in range(self.screen_height):
                if self.x_buffer[x * 2][y * 3]:
                    value = self.screen.get_raw(x, y)
                    self.screen.draw_raw(x, y, value - 64)
                    self.x_buffer[x * 2][y * 3] = False

                if self.x_buffer[x * 2][y * 3 + 1]:
                    value = self.screen.get_raw(x, y)
                    self.screen.draw_raw(x, y, value - 1)
                    self.x_buffer[x * 2][y * 3 + 1] = False

                if self.x_buffer[x * 2][y * 3 + 2]:
                    value = self.screen.get_raw(x, y)
                    self.screen.draw_raw(x, y, value - 8)
                    self.x_buffer[x * 2][y * 3 + 2] = False

                if self.y_buffer[x * 2 + 1][y * 3]:
                    value = self.screen.get_raw(x, y)
                    self.screen.draw_raw(x, y, value - 32)
                    self.y_buffer[x * 2 + 1][y * 3] = False

                if self.y_buffer[x * 2 + 1][y * 3 + 1]:
                    value = self.screen.get_raw(x, y)
                    self.screen.draw_raw(x, y, value - 16)
                    self.y_buffer[x * 2 + 1][y * 3 + 1] = False

                if self.y_buffer[x * 2][y * 3 + 1]:
                    value = self.screen.get_raw(x, y)
                    self.screen.draw_raw(x, y, value - 4)
                    self.y_buffer[x * 2][y * 3 + 1] = False

                if self.y_buffer[x * 2][y * 3]:
                    value = self.screen.get_raw(x, y)
                    self.screen.draw_raw(x, y, value - 2)
                    self.y_buffer[x * 2][y * 3] = False

    def draw_line(self, start_x, start_y, end_x, end_y):
        """
        Draws a line from coordinate to another. To display the line, you must
        use the draw function. This function only updates the underlying data
        buffer.

        start_x -- the starting x point
        start_y -- the starting y point
        end_x -- the ending x point
        end_y -- the ending y point
        """
        start_x = self._constrain(start_x, 0, self.width - 1)
        start_y = self._constrain(start_y, 0, self.height - 1)

        end_x = self._constrain(end_x, 0, self.width - 1)
        end_y = self._constrain(end_y, 0, self.height - 1)

        if end_x < start_x:
            start_x, end_x = end_x, start_x
            start_y, end_y = end_y, start_y

        dx = end_x - start_x
        dy = end_y - start_y

        r = 0
        ny = 0
        pny = 0
        nny = 0

        p = dy / dx if dx != 0 else 0
        t = 0

        for i in range(dx + 1):
            r = int(round(t))
            pny = ny
            ny = start_y + r

            if i > 0:  # vertical lines connecting horizontal lines

                for j in range(abs(ny - pny)):
                    if pny > ny:
                        nny = pny - j - 1
                    else:
                        nny = pny + j

                    self.y_buffer[start_x + i][nny] = 1

            if i != dx:
                self.x_buffer[start_x + i][ny] = 1
            t += p

        if dx == 0 and dy != 0:  # in case of no vertical lines
            fs = 0
            fe = int(dy)

            if dy < 0:
                fs = fe
                fe = 0

            for i in range(fs, fe):
                self.y_buffer[start_x][start_y + i] = 1

    def _constrain(self, val, min_val, max_val):
        """A helper function that constrains a value between two values"""
        return min(max_val, max(min_val, val))

draw(push=True)

Updates the screen with all of the lines drawn using draw_line.

push -- when true all the recent changes are pushed to the display

Source code in display/segment_display.py
def draw(self, push=True):
    """
    Updates the screen with all of the lines drawn using draw_line.

    push -- when true all the recent changes are pushed to the display
    """
    for x in range(self.screen_width):
        for y in range(self.screen_height):

            state = 0

            if self.x_buffer[x * 2][y * 3]:
                state += 64  # 64 = TOP
            if self.x_buffer[x * 2][y * 3 + 1]:
                state += 1  # 1 = CENTER
            if self.x_buffer[x * 2][y * 3 + 2]:
                state += 8  # 8 = BOTTOM

            if self.y_buffer[x * 2 + 1][y * 3]:
                state += 32  # 32 = TR
            if self.y_buffer[x * 2 + 1][y * 3 + 1]:
                state += 16  # 16 = BR
            if self.y_buffer[x * 2][y * 3 + 1]:
                state += 4  # 4 = BL
            if self.y_buffer[x * 2][y * 3]:
                state += 2  # 2 = TL

            # Only draw the pixel if there is something to draw
            if state != 0:
                self.screen.draw_raw(x, y, state)

    if push:
        self.screen.push()

draw_line(start_x, start_y, end_x, end_y)

Draws a line from coordinate to another. To display the line, you must use the draw function. This function only updates the underlying data buffer.

start_x -- the starting x point start_y -- the starting y point end_x -- the ending x point end_y -- the ending y point

Source code in display/segment_display.py
def draw_line(self, start_x, start_y, end_x, end_y):
    """
    Draws a line from coordinate to another. To display the line, you must
    use the draw function. This function only updates the underlying data
    buffer.

    start_x -- the starting x point
    start_y -- the starting y point
    end_x -- the ending x point
    end_y -- the ending y point
    """
    start_x = self._constrain(start_x, 0, self.width - 1)
    start_y = self._constrain(start_y, 0, self.height - 1)

    end_x = self._constrain(end_x, 0, self.width - 1)
    end_y = self._constrain(end_y, 0, self.height - 1)

    if end_x < start_x:
        start_x, end_x = end_x, start_x
        start_y, end_y = end_y, start_y

    dx = end_x - start_x
    dy = end_y - start_y

    r = 0
    ny = 0
    pny = 0
    nny = 0

    p = dy / dx if dx != 0 else 0
    t = 0

    for i in range(dx + 1):
        r = int(round(t))
        pny = ny
        ny = start_y + r

        if i > 0:  # vertical lines connecting horizontal lines

            for j in range(abs(ny - pny)):
                if pny > ny:
                    nny = pny - j - 1
                else:
                    nny = pny + j

                self.y_buffer[start_x + i][nny] = 1

        if i != dx:
            self.x_buffer[start_x + i][ny] = 1
        t += p

    if dx == 0 and dy != 0:  # in case of no vertical lines
        fs = 0
        fe = int(dy)

        if dy < 0:
            fs = fe
            fe = 0

        for i in range(fs, fe):
            self.y_buffer[start_x][start_y + i] = 1

undraw()

Clears all segments drawn both from the screen and from the underlying data structure.

Source code in display/segment_display.py
def undraw(self):
    """
    Clears all segments drawn both from the screen and from the underlying data structure.
    """
    for x in range(self.screen_width):
        for y in range(self.screen_height):
            if self.x_buffer[x * 2][y * 3]:
                value = self.screen.get_raw(x, y)
                self.screen.draw_raw(x, y, value - 64)
                self.x_buffer[x * 2][y * 3] = False

            if self.x_buffer[x * 2][y * 3 + 1]:
                value = self.screen.get_raw(x, y)
                self.screen.draw_raw(x, y, value - 1)
                self.x_buffer[x * 2][y * 3 + 1] = False

            if self.x_buffer[x * 2][y * 3 + 2]:
                value = self.screen.get_raw(x, y)
                self.screen.draw_raw(x, y, value - 8)
                self.x_buffer[x * 2][y * 3 + 2] = False

            if self.y_buffer[x * 2 + 1][y * 3]:
                value = self.screen.get_raw(x, y)
                self.screen.draw_raw(x, y, value - 32)
                self.y_buffer[x * 2 + 1][y * 3] = False

            if self.y_buffer[x * 2 + 1][y * 3 + 1]:
                value = self.screen.get_raw(x, y)
                self.screen.draw_raw(x, y, value - 16)
                self.y_buffer[x * 2 + 1][y * 3 + 1] = False

            if self.y_buffer[x * 2][y * 3 + 1]:
                value = self.screen.get_raw(x, y)
                self.screen.draw_raw(x, y, value - 4)
                self.y_buffer[x * 2][y * 3 + 1] = False

            if self.y_buffer[x * 2][y * 3]:
                value = self.screen.get_raw(x, y)
                self.screen.draw_raw(x, y, value - 2)
                self.y_buffer[x * 2][y * 3] = False

SevenSegment

Source code in display/seven_seg.py
class SevenSegment:
    def __init__(
        self,
        num_digits=8,
        num_per_segment=MAX7219_DIGITS,
        baudrate=DEFAULT_BAUDRATE,
        cs_num=0,
        brightness=7,
        clear=True,
        segment_orientation_array=None,
    ):
        """Constructor

        num_digits -- total number of digits in your display (default 8)
        num_per_segment -- total number of digits per MAX7219 segment (default 8)
        baudrate -- rate at which data is transfered (default 9000kHz), excessive rate may result in instability
        cs_num -- which control select line is being used (default 0)
        brightness -- starting brightness of the leds (default 7)
        clear -- clear the screen on initialization (default True)
        segment_orientation_array -- a 2d array of where the MAX7219 segments are located, one indexed (default None)
            : example [[1, 2], [3, 4], [5, 6], [7, 8], [9, 10], [11, 12]] (height is 6 and width is 16)
            : this needs to be populated to use coordinate grid style functions i.e. letter2 (default None)
        """
        self.num_digits = num_digits
        self.num_segments = num_digits // num_per_segment
        self.num_per_segment = num_per_segment
        self.baudrate = baudrate if baudrate < 10000000 else 10000000
        self._buf = [0] * self.num_digits
        self._display_buf = [0] * self.num_digits
        self._spi = spidev.SpiDev()
        self._spi.open(0, cs_num)
        self._spi.max_speed_hz = self.baudrate

        # Setup the display
        self.command(MAX7219_REG_SHUTDOWN, 1)  # 1 enables the display
        self.command(
            MAX7219_REG_DECODEMODE, 0
        )  # 0x01, 0x0F, 0xFF for different Code B modes
        self.command(MAX7219_REG_SCANLIMIT, self.num_per_segment - 1)
        self.command(MAX7219_REG_DISPLAYTEST, 0)
        self.brightness(brightness)

        # Set up cascaded segemtn orientation stuff to enable 2 functions
        self.display = (
            None or segment_orientation_array
        )  # [[1, 2], [3, 4], [5, 6], [7, 8], [9, 10], [11, 12]]
        self._display_y_len = len(self.display) if self.display is not None else None

        self._flush_index = []
        if clear:
            self.clear()

    def command(self, register_num, value):
        """Sets control registers for each segment in the display"""
        # check register_num is good
        if register_num not in [
            MAX7219_REG_DECODEMODE,
            MAX7219_REG_INTENSITY,
            MAX7219_REG_SCANLIMIT,
            MAX7219_REG_SHUTDOWN,
            MAX7219_REG_DISPLAYTEST,
        ]:
            raise ValueError("register_num is not a correct value")
        # check value is good
        if not isinstance(value, (int)) or (value > 16 or value < 0):
            raise ValueError("value is not a correct value")
        self._write([register_num, value] * self.num_segments)

    def close(self, clear=True, shutdown=True):
        """Close the spi connection"""
        if clear:
            self.clear()
        if shutdown:
            self.command(MAX7219_REG_SHUTDOWN, 0)
        self._spi.close()

    def clear(self, flush=True):
        """Clears the buffer, and if specified, flushes the display"""
        self._buf = [0] * self.num_digits
        if flush:
            self.flush_legacy()

    def brightness(self, value):
        """Sets the brightness for all of the segments ranging from 0 - 15"""
        # check value is good
        if not isinstance(value, (int)) or (value > 16 or value < 0):
            raise ValueError("value is not a correct value")
        self.command(MAX7219_REG_INTENSITY, value)

    # Original flush, about 2 times slower than the current flush function, used in clear
    def flush_legacy(self):
        """Cascade the buffer onto the display"""
        for seg in range(self.num_segments):
            for pos in range(self.num_per_segment):
                self._write(
                    ([MAX7219_REG_NOOP, 0] * (self.num_segments - seg))
                    + [
                        pos + MAX7219_REG_DIGIT0,
                        self._buf[pos + (seg * self.num_per_segment)],
                    ]
                    + ([MAX7219_REG_NOOP, 0] * seg)
                )

    def flush(self):
        """Flush all the current changes to the display"""
        for pos in self._flush_index:
            self._write(
                [MAX7219_REG_NOOP, 0]
                * (self.num_segments - 1 - int(pos / self.num_per_segment))
                + [MAX7219_REG_DIGIT0 + pos % self.num_per_segment, self._buf[pos]]
                + [MAX7219_REG_NOOP, 0] * int(pos / self.num_per_segment)
            )
        self._flush_index.clear()

    def raw(self, position, value, flush=False):
        """Given raw 0-255 value draw symbol at given postion"""
        # Check if position is valid
        if (
            not isinstance(position, (int))
            or position < 0
            or position >= self.num_digits
        ):
            raise ValueError("position is not a valid number")
        # Check if char is int between 0 and 255
        if not isinstance(value, (int)) or value < 0 or value > 255:
            raise ValueError("value is either not an int or out of bounds (0-255)")
        self._buf[position] = value
        self._flush_index.append(position)

        if flush:
            self.flush()

    def raw2(self, x, y, value, flush=False):
        """Given raw 0-255 value draw symbol at given coordinate"""
        position = self._get_pos(x, y)
        self.raw(position, value, flush)

    def letter(self, position, char, dot=False, flush=False):
        """Outputs ascii letter as close as it can, working letters/symbols found in symbols.py"""
        # Check if position is valid
        if (
            not isinstance(position, (int))
            or position < 0
            or position >= self.num_digits
        ):
            raise ValueError("position is not a valid number")
        value = sy.get_char2(char) | (dot << 7)
        self._buf[position] = value
        self._flush_index.append(position)
        if flush:
            self.flush()

    def letter2(self, x, y, char, dot=False, flush=False):
        """Output letter on the display at the coordinates provided if possible"""
        # Check to make sure segment array has been initialized
        if self.display is None:
            raise ValueError("segment_orientation_array has not been initialized")
        pos = self._get_pos(x, y)
        self.letter(pos, char, dot, flush)

    def text(self, txt, start_position=0, flush=False):
        """Output text on the display at the start position if possible"""
        # Check if txt is going to overflow buffer
        if start_position + len(txt.replace(".", "")) > self.num_digits:
            raise OverflowError("Message would overflow spi buffer")

        for pos, char in enumerate(txt):
            # Check if current char is a dot and append to previous letter
            if char == "." and pos != 0:  # mutliple dots in a row cause an error
                self.letter(pos + start_position - 1, txt[pos - 1], dot=True)
            else:
                self.letter(start_position + pos, char)

        if flush:
            self.flush()

    def text2(self, x, y, txt, horizontal=True, flush=False):
        """Output text on the display at the given x, y - option to display horizontal or vertical text"""
        # No initial checks and will let underlying functions do the work
        if horizontal:
            # self.text(txt, self._get_pos(x, y))
            for pos, char in enumerate(txt):
                # Check if current char is a dot and append to previous letter
                if char == "." and pos != 0:  # mutliple dots in a row cause an error
                    self.letter2(x + pos - 1, y, txt[pos - 1], True)
                else:
                    self.letter2(x + pos, y, char)
        else:
            for pos, char in enumerate(txt):
                # Check if current char is a dot and append to previous letter
                if char == "." and pos != 0:  # mutliple dots in a row cause an error
                    self.letter2(x, y + pos - 1, txt[pos - 1], True)
                else:
                    self.letter2(x, y + pos, char)
        if flush:
            self.flush()

    # Write to the SPI file through SPI library
    def _write(self, data):
        self._spi.writebytes(bytes(data))

    # Get position in the buffer for a given x,y coordinate
    def _get_pos(self, x, y):
        # Check y is within bounds
        if not isinstance(y, (int)) or y < 0 or y >= self._display_y_len:
            return ValueError("y value is not a valid number")

        # Check if x is an int
        if not isinstance(x, (int)):
            return ValueError("x value is not an integer")
        x_seg = int(x / self.num_per_segment)

        # check if x is within bounds of y row
        if x_seg >= len(self.display[y]):
            raise ValueError("x value is out of range")

        return (self.display[y][x_seg] - 1) * self.num_per_segment + (
            x % self.num_per_segment
        )

    # Not current in use
    def _check_buf(self):
        indices = []
        for pos in range(len(self._buf)):
            if self._buf[pos] != self._display_buf[pos]:
                indices.append(pos)
        return indices

__init__(num_digits=8, num_per_segment=MAX7219_DIGITS, baudrate=DEFAULT_BAUDRATE, cs_num=0, brightness=7, clear=True, segment_orientation_array=None)

Constructor

num_digits -- total number of digits in your display (default 8) num_per_segment -- total number of digits per MAX7219 segment (default 8) baudrate -- rate at which data is transfered (default 9000kHz), excessive rate may result in instability cs_num -- which control select line is being used (default 0) brightness -- starting brightness of the leds (default 7) clear -- clear the screen on initialization (default True) segment_orientation_array -- a 2d array of where the MAX7219 segments are located, one indexed (default None) : example [[1, 2], [3, 4], [5, 6], [7, 8], [9, 10], [11, 12]] (height is 6 and width is 16) : this needs to be populated to use coordinate grid style functions i.e. letter2 (default None)

Source code in display/seven_seg.py
def __init__(
    self,
    num_digits=8,
    num_per_segment=MAX7219_DIGITS,
    baudrate=DEFAULT_BAUDRATE,
    cs_num=0,
    brightness=7,
    clear=True,
    segment_orientation_array=None,
):
    """Constructor

    num_digits -- total number of digits in your display (default 8)
    num_per_segment -- total number of digits per MAX7219 segment (default 8)
    baudrate -- rate at which data is transfered (default 9000kHz), excessive rate may result in instability
    cs_num -- which control select line is being used (default 0)
    brightness -- starting brightness of the leds (default 7)
    clear -- clear the screen on initialization (default True)
    segment_orientation_array -- a 2d array of where the MAX7219 segments are located, one indexed (default None)
        : example [[1, 2], [3, 4], [5, 6], [7, 8], [9, 10], [11, 12]] (height is 6 and width is 16)
        : this needs to be populated to use coordinate grid style functions i.e. letter2 (default None)
    """
    self.num_digits = num_digits
    self.num_segments = num_digits // num_per_segment
    self.num_per_segment = num_per_segment
    self.baudrate = baudrate if baudrate < 10000000 else 10000000
    self._buf = [0] * self.num_digits
    self._display_buf = [0] * self.num_digits
    self._spi = spidev.SpiDev()
    self._spi.open(0, cs_num)
    self._spi.max_speed_hz = self.baudrate

    # Setup the display
    self.command(MAX7219_REG_SHUTDOWN, 1)  # 1 enables the display
    self.command(
        MAX7219_REG_DECODEMODE, 0
    )  # 0x01, 0x0F, 0xFF for different Code B modes
    self.command(MAX7219_REG_SCANLIMIT, self.num_per_segment - 1)
    self.command(MAX7219_REG_DISPLAYTEST, 0)
    self.brightness(brightness)

    # Set up cascaded segemtn orientation stuff to enable 2 functions
    self.display = (
        None or segment_orientation_array
    )  # [[1, 2], [3, 4], [5, 6], [7, 8], [9, 10], [11, 12]]
    self._display_y_len = len(self.display) if self.display is not None else None

    self._flush_index = []
    if clear:
        self.clear()

brightness(value)

Sets the brightness for all of the segments ranging from 0 - 15

Source code in display/seven_seg.py
def brightness(self, value):
    """Sets the brightness for all of the segments ranging from 0 - 15"""
    # check value is good
    if not isinstance(value, (int)) or (value > 16 or value < 0):
        raise ValueError("value is not a correct value")
    self.command(MAX7219_REG_INTENSITY, value)

clear(flush=True)

Clears the buffer, and if specified, flushes the display

Source code in display/seven_seg.py
def clear(self, flush=True):
    """Clears the buffer, and if specified, flushes the display"""
    self._buf = [0] * self.num_digits
    if flush:
        self.flush_legacy()

close(clear=True, shutdown=True)

Close the spi connection

Source code in display/seven_seg.py
def close(self, clear=True, shutdown=True):
    """Close the spi connection"""
    if clear:
        self.clear()
    if shutdown:
        self.command(MAX7219_REG_SHUTDOWN, 0)
    self._spi.close()

command(register_num, value)

Sets control registers for each segment in the display

Source code in display/seven_seg.py
def command(self, register_num, value):
    """Sets control registers for each segment in the display"""
    # check register_num is good
    if register_num not in [
        MAX7219_REG_DECODEMODE,
        MAX7219_REG_INTENSITY,
        MAX7219_REG_SCANLIMIT,
        MAX7219_REG_SHUTDOWN,
        MAX7219_REG_DISPLAYTEST,
    ]:
        raise ValueError("register_num is not a correct value")
    # check value is good
    if not isinstance(value, (int)) or (value > 16 or value < 0):
        raise ValueError("value is not a correct value")
    self._write([register_num, value] * self.num_segments)

flush()

Flush all the current changes to the display

Source code in display/seven_seg.py
def flush(self):
    """Flush all the current changes to the display"""
    for pos in self._flush_index:
        self._write(
            [MAX7219_REG_NOOP, 0]
            * (self.num_segments - 1 - int(pos / self.num_per_segment))
            + [MAX7219_REG_DIGIT0 + pos % self.num_per_segment, self._buf[pos]]
            + [MAX7219_REG_NOOP, 0] * int(pos / self.num_per_segment)
        )
    self._flush_index.clear()

flush_legacy()

Cascade the buffer onto the display

Source code in display/seven_seg.py
def flush_legacy(self):
    """Cascade the buffer onto the display"""
    for seg in range(self.num_segments):
        for pos in range(self.num_per_segment):
            self._write(
                ([MAX7219_REG_NOOP, 0] * (self.num_segments - seg))
                + [
                    pos + MAX7219_REG_DIGIT0,
                    self._buf[pos + (seg * self.num_per_segment)],
                ]
                + ([MAX7219_REG_NOOP, 0] * seg)
            )

letter(position, char, dot=False, flush=False)

Outputs ascii letter as close as it can, working letters/symbols found in symbols.py

Source code in display/seven_seg.py
def letter(self, position, char, dot=False, flush=False):
    """Outputs ascii letter as close as it can, working letters/symbols found in symbols.py"""
    # Check if position is valid
    if (
        not isinstance(position, (int))
        or position < 0
        or position >= self.num_digits
    ):
        raise ValueError("position is not a valid number")
    value = sy.get_char2(char) | (dot << 7)
    self._buf[position] = value
    self._flush_index.append(position)
    if flush:
        self.flush()

letter2(x, y, char, dot=False, flush=False)

Output letter on the display at the coordinates provided if possible

Source code in display/seven_seg.py
def letter2(self, x, y, char, dot=False, flush=False):
    """Output letter on the display at the coordinates provided if possible"""
    # Check to make sure segment array has been initialized
    if self.display is None:
        raise ValueError("segment_orientation_array has not been initialized")
    pos = self._get_pos(x, y)
    self.letter(pos, char, dot, flush)

raw(position, value, flush=False)

Given raw 0-255 value draw symbol at given postion

Source code in display/seven_seg.py
def raw(self, position, value, flush=False):
    """Given raw 0-255 value draw symbol at given postion"""
    # Check if position is valid
    if (
        not isinstance(position, (int))
        or position < 0
        or position >= self.num_digits
    ):
        raise ValueError("position is not a valid number")
    # Check if char is int between 0 and 255
    if not isinstance(value, (int)) or value < 0 or value > 255:
        raise ValueError("value is either not an int or out of bounds (0-255)")
    self._buf[position] = value
    self._flush_index.append(position)

    if flush:
        self.flush()

raw2(x, y, value, flush=False)

Given raw 0-255 value draw symbol at given coordinate

Source code in display/seven_seg.py
def raw2(self, x, y, value, flush=False):
    """Given raw 0-255 value draw symbol at given coordinate"""
    position = self._get_pos(x, y)
    self.raw(position, value, flush)

text(txt, start_position=0, flush=False)

Output text on the display at the start position if possible

Source code in display/seven_seg.py
def text(self, txt, start_position=0, flush=False):
    """Output text on the display at the start position if possible"""
    # Check if txt is going to overflow buffer
    if start_position + len(txt.replace(".", "")) > self.num_digits:
        raise OverflowError("Message would overflow spi buffer")

    for pos, char in enumerate(txt):
        # Check if current char is a dot and append to previous letter
        if char == "." and pos != 0:  # mutliple dots in a row cause an error
            self.letter(pos + start_position - 1, txt[pos - 1], dot=True)
        else:
            self.letter(start_position + pos, char)

    if flush:
        self.flush()

text2(x, y, txt, horizontal=True, flush=False)

Output text on the display at the given x, y - option to display horizontal or vertical text

Source code in display/seven_seg.py
def text2(self, x, y, txt, horizontal=True, flush=False):
    """Output text on the display at the given x, y - option to display horizontal or vertical text"""
    # No initial checks and will let underlying functions do the work
    if horizontal:
        # self.text(txt, self._get_pos(x, y))
        for pos, char in enumerate(txt):
            # Check if current char is a dot and append to previous letter
            if char == "." and pos != 0:  # mutliple dots in a row cause an error
                self.letter2(x + pos - 1, y, txt[pos - 1], True)
            else:
                self.letter2(x + pos, y, char)
    else:
        for pos, char in enumerate(txt):
            # Check if current char is a dot and append to previous letter
            if char == "." and pos != 0:  # mutliple dots in a row cause an error
                self.letter2(x, y + pos - 1, txt[pos - 1], True)
            else:
                self.letter2(x, y + pos, char)
    if flush:
        self.flush()

Last update: July 13, 2022
Created: July 13, 2022