Virtual display – part 2: offsets

We have a simple virtual display. It can join lcds in one virtual area. But there is one catch. Look at the following layouts (red area is virtual lcd):

layout_leftlayout_center

No problem. But what about:

layout_right

… we can’t do it ! And what if we want to use this small 4×4 as second virtual display ? We can’t !

When we add lcd its added at any (x,y) but always starts from (0,0). Lets add an offset, so we can specify area.

Update – error in code. see at the bottom

Download source

Offset

First lets change our abstract lcd_virtual.py. Find function add_display and get_display change them to:


    def add_display(self, pos_x, pos_y, display, offset_x=0, offset_y=0):
        """add lcd to virtual display"""
        self.displays.append({
            'x': pos_x,
            'y': pos_y,
            'offset_x': offset_x,
            'offset_y': offset_y,
            'width': display.get_width(),
            'height': display.get_height(),
            'lcd': display
        })

    def get_display(self, pos_x, pos_y):
        """return display for (x,y) coords"""
        if pos_x > self.width or pos_x < 0 or pos_y > self.height or pos_y < 0:
            raise IndexError

        for display in self.displays:
            if display['x'] <= pos_x < display['x'] + display['width'] - display['offset_x'] and \
                display['y'] <= pos_y < display['y'] + display['height'] - display['offset_y']:
                return display
        return None

Direct access

Time to add offset functionality to main classes. First lets change direct one. We can grab a correct lcd but we need to change start point on lcd. Replace function _select_active_display in lcd_virtual_direct.py with:


     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.active_display['offset_x'],
                self.c_h - self.active_display['y'] + self.active_display['offset_y']
            )

We have added offsets when setting position_xy. Now we will select correct display and correct start point. Last thing is to change char function. See last if in it ? We need to add offset_x so we know that we are out of bounds. New function looks like this:


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

        self._select_active_display()

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

        self.active_display['lcd'].char(char)

        if self.c_w >= self.active_display['x'] + \
                self.active_display['width'] - 1 - self.active_display['offset_x']:
            self.active_display = None

And to check if we done good job (of course we also have unit tests) modify our test script:


#!/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_test1():
    """test 1 - top, down"""
    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")


def main_test2():
    """test 2 - left, right"""
    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.add_display(0, 0, lcd_2)
    vlcd_1.add_display(20, 0, lcd_1)
    vlcd_1.string('test me 123456789qwertyuiopasdfghjkl12')

def main_test3():
    """test 3 - offset top,down"""
    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(16, 6)
    vlcd_1.add_display(0, 0, lcd_2, 4, 0)
    vlcd_1.add_display(0, 4, lcd_1)

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

def main_test4():
    """test 4 - offset left right """
    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(10, 2)
    vlcd_1.add_display(0, 0, lcd_1, 11)
    vlcd_1.add_display(5, 0, lcd_2)

    vlcd_1.position_xy(0, 0)
    vlcd_1.string('1234567890123456')
    vlcd_1.position_xy(0, 1)
    vlcd_1.char('/')

    vlcd_1.position_xy(9, 1)
    vlcd_1.char('*')

    vlcd_1.position_xy(0, 0)
    vlcd_1.char('-')

    vlcd_1.position_xy(9, 0)
    vlcd_1.char('#')

    vlcd_1.position_xy(5, 1)
    vlcd_1.char('!')

    vlcd_1.position_xy(4, 1)
    vlcd_1.char('+')

main_test4()


Tests were split into functions. It should be like that from beginning. Now is easy to see result and not only bare tests.

Buffered access

If you thing that we only need to add offset variables that.. you are almost right. There is one catch. Function flush. Remember that we may have two virtual lcds on one lcd. Flush will flush physical lcd. If we forget about this and do flushing one virtual and second virtual we will in fact do flush twice on real one. We won’t fix this problem but do some small improvement.

This is surprise, we need to change only three lines. Open lcd_virtual_buffered.py find function _char and spot differences:


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

        self._select_active_display(pos_x, pos_y)

        if self.active_display is None:
            return

        self.active_display['lcd'].write(
            char,
            pos_x - self.active_display['x'] + self.active_display['offset_x'],
            pos_y - self.active_display['y'] + self.active_display['offset_y']
        )

        if pos_x >= self.active_display['x'] + \
                self.active_display['width'] - 1 - self.active_display['offset_x']:
            self.active_display = None

I just discovered problem with clear_buffer (in file lcd_buffeded.py). In case of two vlcd on one lcd, when we clear any vlcd buffer both buffers are cleared. Not good.

But first we will modify flushing. Idea is to add dirty flag and set true only if we change something in buffer. Flushing will first check this flag and if set to false just return without any action. Its much faster than dry run in loop. But it’s easy so I won’t show it here, check source code ๐Ÿ™‚

Back to clear_buffer, what we want is ability to clear rectangle area in buffer. This will add more logic to the function so lets spit this into two cases. One will work as before and if additional parameters are send use more advanced algorithm.

New function:


    def buffer_clear(self, from_x=None, from_y=None, width=None, height=None):
        """clears buffer"""
        if from_x is None and from_y is None:
            self.buffer = [" ".ljust(self.width, " ")
                           for _ in range(0, self.height)]
        else:
            if height is None:
                height = self.height - from_y
            if width is None:
                width = self.width - from_y
            for pos_y in range(from_y, from_y + height):
                line = self.buffer[pos_y]
                self.buffer[pos_y] = line[0:from_x] + \
                                     "".ljust(width, " ") + \
                                     line[from_x + width:]
        self.dirty = True

Virtual function is much more complicated. Area we select for clearing may spread to many lcds. And we need to recalculate where which part of area is. But first we need to select those parts. Hard task. For now this function won’t work while clearing area of virtual display. But will work for clearing whole virtual buffer and clearing selected area on buffers. New function :


    def buffer_clear(self, from_x=None, from_y=None, width=None, height=None):
        """clears buffer"""        
        self.buffer = [" ".ljust(self.width, " ")
                       for _ in range(0, self.height)]

        for display in self.displays:
            display['lcd'].buffer_clear(
                display['x'],
                display['y'],
                display['width'] - display['offset_x'],
                display['height'] - display['offset_y']
            )

Refactoring – directories

When we look at the files in project root directory we will see many files. At the beginning there were simple demo scripts but now we have quite a mess there.

Lets take all lcd related files and put them where they belong, to charlcd/demos directory. Four files: lcd_buffered.py, lcd_direct.py, lcd_virtual_buffered.py and lcd_virtual_direct.py. They just need one modification, open each file and add just before first import:

import sys
sys.path.append("../../")

This will modify paths and allow us to import package from parent directory.

And now we have all lcd project files in nice charlcd package.

Refactoring – function names.

Lets look again at names in each access type (buffered, direct). In direct we have: string, char, position_xy, stream_char and stream_string. In buffered:ย init, write, set_xy, get_xy, buffer_clear, flush. Question is why functions that do same are called differently ? Im talking about set_xy vs position_xy and write vs char and string.

We need to clean this.

Firstย lcd_direct.py . Function string rename to write and add position arguments. Function postion_xy change to set_xy. Next change char to _char and stream_char to _stream_char. Last step rename stream_string to stream.

Remember we need to do changes also in direct_interface.py. Three function will stay there: write, set_xy and stream.

There was problem with coordinates, see that only stream function works with position. So when write was used script lost track of correct coordinates and using stream after that created strange results. Now each function works with internal coordinates.

One file ready, open lcd_virtual_direct.py and do similar modifications.

Now we have two different ways of storing current position. We have current_pos vs c_w and c_h. Change it so only current_pos is left.

The code looks and works much better.

Refactoring – classes and inheritance

We may clean some more of code. When you look at direct_interface and buffered_interface you will spot code duplication. I’m talking about two functions: write and set_xy. Move those functions from direct_interface to lcd.py in abstract sub-package. Remove them from buffered_interface.py.

I made few more small changes but forgot to write them down, sorry ๐Ÿ™‚

Stop ! Jenkins time!

Its this time already ๐Ÿ™‚ Quick automated code review. This time we failed with:

  • missing comments
  • didn’t change functions in abstract classes (clear_buffer)
  • non inheritance of stream and write function in lcd_virtual
  • lots of code duplication in test and demos directory – ignore it

Summary

This time we have:

  • added offset when adding lcd to vlcd
  • refactored functions
  • refactored classes
  • failed with proper buffer_clear in vlcd
  • moved all lcd files to one package

Download source

Update

There is an error in code, file lcd_virtual_biffered.py function buffer_clear. Correct is:


     def buffer_clear(self, from_x=None, from_y=None, width=None, height=None):
        """clears buffer"""       
        self.buffer = [" ".ljust(self.width, " ")
                       for _ in range(0, self.height)]

        for display in self.displays:
            display['lcd'].buffer_clear(
                display['offset_x'],
                display['offset_y'],
                display['width'] - display['offset_x'],
                display['height'] - display['offset_y']
            )

Advertisements

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