Virtual display – part 1

We have a fully functional game that is quite primitive but don’t wory. We will do something about it and during this we will upgrade Lcd classes.

Remember draw function in game.py ? It was doing all kind of checks and distinctions between lcds. It shouldn’t be like that. Game class shouldn’t care about proper display, such task goes to lower level of code. From game perspective it should have some area to display and thats all. It should write on any (x, y) allowed by combined displays and don’t care which display is top, bottom, left or right.

Its time for Virtual Display šŸ™‚

Download source

Virtual display

Current goal is to create virtual display that will be made from any number of lcds. We will define virtual size and position (x, y) for each display. We will do both versions, buffered and direct.

To group all abstract classes lets create abstract package in charlcd and move there lcd.py. Then create lcd_virtual.py it will be base class for virtual displays. We want to CharLCDVirtual to be transparent so it will implement all functions that CharLCD have.

Now in package charlcd we have file structure as this:

start_file_structure

 

 

 

 

To make live easier lets inherit CharLCD and direct_interface.py. But first we need to create it šŸ™‚ So create direct_interface.py in abstract directory and copy:


#!/usr/bin/python
# -*- coding: utf-8 -*-
#pylint: disable=I0011,R0913,R0902

"""Interface for direct inputs"""


class Direct(object):
    def string(self, string):
        """Write string to lcd
        Args:
            string: string to display
        """
        raise NotImplementedError("string not implemented")

    def char(self, char):
        """Write char
        Args:
            char: char to display
        """
        raise NotImplementedError("char not implemented")

    def position_xy(self, pos_x, pos_y):
         """Set cursor position to (x, y)
        Args:
            pos_x: x position
            pos_y: y position
        """
        raise NotImplementedError("position_xy not implemented")

    def stream_char(self, char):
         """Stream char on screen, following chars are put one after another.
        Restart from beginning after reaching end
        Args:
            char: char to display
        """
        raise NotImplementedError("stream_char not implemented")

    def stream_string(self, string):
        """Stream string - use stream_char
        Args:
            string: string to display
        """
        raise NotImplementedError("stream_string not implemented")


And modify class in lcd_direct.py, add


import charlcd.abstract.direct_interface as direct

and change declaration to:


class CharLCD(lcd.CharLCD, direct.Direct):

Virtual direct display

Create lcd_virtual_direct.py in charlcd package. We will start with bare class:


#!/usr/bin/python
# -*- coding: utf-8 -*-

import charlcd.abstract.lcd_virtual as lcd
import charlcd.abstract.direct_interface as direct


class CharLCD(lcd.CharLCDVirtual, direct.Direct):
    def __init__(self, width, height):
        lcd.CharLCDVirtual.__init__(self,
                                    width, height,
                                    lcd.DISPLAY_MODE_DIRECT)

Next time to create test script in root, with same name, and paste:

#!/usr/bin/python
# -*- coding: utf-8 -*-

"""test script for virtual direct lcd"""

import RPi.GPIO as GPIO #pylint: disable=I0011,F0401
from charlcd import lcd_direct as lcd
from charlcd.drivers.gpio import Gpio
from charlcd.drivers.i2c import I2C #pylint: disable=I0011,F0401
from charlcd import lcd_virtual_direct as vlcd

GPIO.setmode(GPIO.BCM)


def main():
    """main loop"""
    lcd_1 = lcd.CharLCD(16, 2, I2C(0x20, 1))
    lcd_2 = lcd.CharLCD(20, 4, Gpio(), 0, 0)
    lcd_1.init()
    lcd_2.init()

    vlcd_1 = vlcd.CharLCD(20,6)
    vlcd_1.add_display(0, 0, lcd_2)
    vlcd_1.add_display(2, 4, lcd_1)

    vlcd_1.string('test me 123456789qwertyuiop')

    vlcd_1.position_xy(2, 0)
    vlcd_1.char('1')
    vlcd_1.position_xy(2, 1)
    vlcd_1.char('2')
    vlcd_1.position_xy(2, 2)
    vlcd_1.char('3')
    vlcd_1.position_xy(2, 3)
    vlcd_1.char('4')
    vlcd_1.position_xy(2, 4)
    vlcd_1.char('5')
    vlcd_1.position_xy(2, 5)
    vlcd_1.char('6')

    vlcd_1.position_xy(3, 4)
    vlcd_1.string('-Second blarg !')
    vlcd_1.position_xy(4, 5)
    vlcd_1.string("-second line")

    vlcd_1.position_xy(11, 3)
    vlcd_1.stream_string("two liner and screener")

main()

Now when you run it – it will fail. We have only empty functions. Those function require body, lets get to work šŸ™‚

Direct input require 5 functions: string, char, position_xy, stream_char and stream_string. When using hardware lcds stream_* were different from non-stream. But now they will work same way, because we need to keep track of cursor position. While using string in lcd, chipset was changing position internally but now we have few lcds and need to keep track of everything.

What we need to do is find proper lcd for virtual (x, y), recalculate (x, y) for given lcd and display char. After that increase internal x pointer and check if we are out of screen. If yes find new one. Funny thing is that proper lcd may not exist. If you look at Piader layout, 20×4 on top and 16×2 bottom with 2 padding, you will see that position (0,5) has no lcd šŸ™‚ We will allow such situation without raising any exception.

Lets see our new functions:


    def string(self, string):
        """Write string to lcd
        Args:
            string: string to display
        """
        for char in string:
            self.char(char)

    def char(self, char):
        """Write char
        Args:
            char: char to display
        """
        if self.c_w >= self.width:
            return

        self._select_active_display()

        if self.active_display is None:
            self.c_w += 1
            return

        self.c_w += 1
        self.active_display['lcd'].char(char)

        if self.c_w >= self.active_display['x'] + self.active_display['width'] - 1:
            self.active_display = self.get_display(self.c_w, self.c_h)

    def position_xy(self, pos_x, pos_y):
        """Set cursor position @ (x, y)
        Args:
            pos_x: x position
            pos_y: y position
        """
        self.c_w = pos_x
        self.c_h = pos_y
        self.active_display = None

    def stream_char(self, char):
        """Stream char on screen, following chars are put one after another.
        Restart from beginning after reaching end
        Args:
            char: char to display
        """
        self.char(char)
        if self.c_w >= self.width:
            self.c_w = 0
            self._inc_c_w()

    def stream_string(self, string):
        """Stream string - use stream_char
        Args:
            string: string to display
        """
        for char in string:
            self.stream_char(char)

    def _inc_c_w(self):
        """Helper, calculate next char position while streaming
        """
        self.c_h += 1
        self.active_display = None
        if self.c_h >= self.height:
            self.c_h = 0

    def _select_active_display(self):
        """Helper, look for proper lcd for given coordinates, may return None"""
        if self.active_display is None:
            self.active_display = self.get_display(self.c_w, self.c_h)
            if self.active_display is None:
                return
            self.active_display['lcd'].position_xy(
                self.c_w - self.active_display['x'],
                self.c_h - self.active_display['y']
            )

Now after running test script everything works. We have virtual screen for direct operations.

Virtual buffered display

Time for buffered option. We start same as with direct, common functions are: init, write, set_xy, get_xy, buffer_clear, flush. Create file buffered_interface.py in abstract:


class Buffered(object):
    def init(self):
        """screen and buffer init"""
        raise NotImplementedError("init not implemented")

    def write(self, content, pos_x=None, pos_y=None):
        """Writes content into buffer at position(x,y) or current
        Will change internal position marker to reflect string write
        Args:
            content: content to write
            pos_x: x position
            pos_y: y position
        """
        raise NotImplementedError("write not implemented")

    def set_xy(self, pos_x, pos_y):
        """sets current cursor position
        Args:
            pos_x: x position
            pos_y: y position
        """
        raise NotImplementedError("set_xy not implemented")

    def get_xy(self):
        """return current cursor position"""
        raise NotImplementedError("get_xy not implemented")

    def buffer_clear(self):
        """clears buffer"""
        raise NotImplementedError("buffer_clear not implemented")

    def flush(self):
        """Flush buffer to screen, skips chars that didn't change"""
        raise NotImplementedError("flush not implemented")

Next quick modification in lcd_direct.py, change so class will inherit Buffered.

We have simpler task than before, why ? Because buffered won’t talk directly with hardware, we just need to modify functions that they work on buffer on proper screen.

Create lcd_virtual_buffered.py in charlcd package. We will start with inits:

import charlcd.abstract.lcd_virtual as lcd
import charlcd.abstract.buffered_interface as buffered_interface


class CharLCD(lcd.CharLCDVirtual, buffered_interface.Buffered):
    screen = []
    buffer = []
    current_pos = {'x': 0,
                   'y': 0}

    def __init__(self, width, height):
        lcd.CharLCDVirtual.__init__(self,
                                    width, height,
                                    lcd.DISPLAY_MODE_BUFFERED)
        self.active_display = None

     def init(self):
        """Buffer init"""
        self.screen = [" ".ljust(self.width, " ")
                       for _ in range(0, self.height)]
        self.buffer = [" ".ljust(self.width, " ")
                       for _ in range(0, self.height)]

I’m not so sure about init function because it will create buffer that will be duplicated. But maybe it won’t be bad to keep everything as one in big buffer ? We will see šŸ™‚

Another problems comes from write function. We need it to be clever if text start on None but some chars hits proper LCD we need to know it. So no easy going :/ We have to check each char in given string. We are lucky that those chars are not displayed but only put to buffer because its not heavy operation.

Simple functions goes as first:


     def set_xy(self, pos_x, pos_y):
        """sets current cursor position
        Args:
            pos_x: x position
            pos_y: y position
        """
        if pos_x >= self.width:
            raise IndexError
        if pos_y >= self.height:
            raise IndexError
        self.current_pos['x'] = pos_x
        self.current_pos['y'] = pos_y

    def get_xy(self):
        """return current cursor position"""
        return self.current_pos

    def buffer_clear(self):
        """clears buffer"""
        self.buffer = [" ".ljust(self.width, " ")
                       for _ in range(0, self.height)]
        for lcd in self.displays:
            lcd['lcd'].buffer_clear()

    def flush(self):
        """Flush buffer to screen, skips chars that didn't change"""
        for lcd in self.displays:
            lcd['lcd'].flush()

Function write is not as much complicated as I thought, what we need to do is write proper char to proper buffer. So we need function to find proper lcd for (x, y) – we have it.


    def write(self, content, pos_x=None, pos_y=None):
        if pos_x is None:
            pos_x = self.current_pos['x']
        if pos_y is None:
            pos_y = self.current_pos['y']

        if pos_x >= self.width:
            raise IndexError
        if pos_y >= self.height:
            raise IndexError

        line = self.buffer[pos_y]
        new_line = line[0:pos_x] + content + line[pos_x + len(content):]
        line = new_line[:self.width]
        self.buffer[pos_y] = line
        self.current_pos = {'x': pos_x + len(content),
                            'y': pos_y}

        for char in content:
            self._char(char, pos_x, pos_y)
            pos_x += 1

    def _char(self, char, x, y):
        """Write char to proper lcd buffer
        Args:
            char: char to write
        """
        if x >= self.width:
            return

        self._select_active_display(x, y)

        if self.active_display is None:
            return

        self.active_display['lcd'].write(
            char,
            x - self.active_display['x'],
            y - self.active_display['y']
        )

        if x >= self.active_display['x'] + self.active_display['width'] - 1:
            self.active_display = self.get_display(x, y)

    def _select_active_display(self, x, y):
        """Helper, look for proper lcd for given coordinates, may return None"""
        self.active_display = self.get_display(x, y)


Seems simple.. Create quick test script in root folder, lcd_virtual_buffered.py, and copy:


#!/usr/bin/python
# -*- coding: utf-8 -*-

"""test script for virtual buffered lcd"""


import RPi.GPIO as GPIO #pylint: disable=I0011,F0401
from charlcd import lcd_buffered as lcd
from charlcd.drivers.gpio import Gpio
from charlcd.drivers.i2c import I2C
from charlcd import lcd_virtual_buffered as vlcd

GPIO.setmode(GPIO.BCM)


def main():
    """main loop"""
    lcd_1 = lcd.CharLCD(16, 2, I2C(0x20, 1))
    lcd_2 = lcd.CharLCD(20, 4, Gpio(), 0, 0)
    lcd_1.init()
    lcd_2.init()

    # vlcd_1 = vlcd.CharLCD(36,4)
    # vlcd_1.init()
    # vlcd_1.add_display(0, 0, lcd_2)
    # vlcd_1.add_display(20, 0, lcd_1)
    # vlcd_1.write('test me 123456789qwertyuiopasdfghjkl12')

    vlcd_1 = vlcd.CharLCD(20,6)
    vlcd_1.init()

    vlcd_1.add_display(0, 0, lcd_2)
    vlcd_1.add_display(0, 4, lcd_1)

    vlcd_1.write('First line')
    vlcd_1.write('Second line', 0, 1)
    vlcd_1.write('Fifth Line', 0, 4)

    vlcd_1.set_xy(4, 2)
    vlcd_1.write('third line')

    vlcd_1.flush()

main()

For me everything was working šŸ™‚

Stop ! Jenkins time !

Time for cleaning code and some refactoring … or not, Virtual Machine with Jenkins died…

I upgraded to Windows 10 and enabled HyperV so VirtualBox lost its access to hardware virtualization. But why today ? It was working when last used. Ok we will do something about it. Maybe Vagrant with simple shell privisioning.. hmm…

Summary

We have created two classes that can act as virtual display. They use physical hardware. But there is one functionality missing, we can’t create two virtuals from one lcd. See that we start from physical (0, 0), its good idea to add offset …

Download source

Advertisements

One comment

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s