ProxyLCD and graphical LCDs with HD44780 driver

Last time we wrote an HD44780 driver that takes GfxLCD compatible display and works with CharLCD package. This is nice because we can use big ILI9486 as a character display.

You may ask, why?
Because with ProxyLCD we can see a big output from Symfony 🙂 And because we can 🙂
Today we will focus on two bugs from a previous part and we will see if all chipsets can work with HD44780 driver.

Part 1

ProxyLCD config

My first discovery was that package CharLCD on PyPi is old! I didn’t upgrade the library for some time! Baka me.

With working driver, we may finally hook display and remote LCD to Symfony.
Remember the code from the top of the previous part? Let’s copy it and swap a driver:

import RPi.GPIO
from message_listener.server import Server
from iot_message.message import Message
from charlcd.handler import Handler
from charlcd.buffered import CharLCD
from gfxlcd.driver.ili9325.gpio import GPIO as ILIGPIO
from gfxlcd.driver.ili9325.ili9325 import ILI9325
from gfxlcd.driver.hd44780 import HD44780
RPi.GPIO.setmode(RPi.GPIO.BCM)

ili_drv = ILIGPIO()
ili_drv.pins['LED'] = 6
ili_drv.pins['CS'] = 18
lcd_ili = ILI9325(240, 320, ili_drv)
lcd_ili.auto_flush = False
lcd_ili.rotation = 0

drv = HD44780(lcd_ili)
lcd = CharLCD(drv.width, drv.height, drv, 0, 0)
lcd.init()

msg = Message('rpi2')

svr = Server(msg)
svr.add_handler('GfxLCD', Handler(lcd))
svr.run()

svr.join()

What we changed is a driver passed to CharLCD.
Nice, what now? Add node configuration to app config:

[lcd3]
name = GfxLCD
size = 30x40
node_name = rpi2
stream = 1
type = charlcd

And… fail… what the?
Change the height to 15 and… it is ok. Quick debug and…we have the hard coded value 1024 bytes in message_listener!

data, address = self.socket.recvfrom(1024)

in /message_listener/server.py, line 41.

Another case when hard coded value hit us back 🙂 Lesson to learn.
But is this it? I changed 1024 to 2048 and… it is ok, display works.

How to fix this? From what I read and know there is no way of reading UDP data in parts. When you read data from the socket, it is discarded even if you didn’t read the whole buffer. This is how Python works.

So, what can we do?
We may try and always read the maximum packet size: 65535. Or even better add a parameter to set buffer size with default 64k.

Ok. Let’s use this big screen. We will dump the whole request in Symfony. In Sonata admin service I have:

dump($this->getRequest());

and…. nothing. App crashed.

What the? Strikes back.

PyQt hides errors but with a small trick I got:

EXCEPTION IN (D:\workspace\lcd-proxy\model\display.py, LINE 24 "self.lcd.flush()"): list index out of range

We need to go deeper…and I found it! In function flush() is a call to get_line_address and it tries to get an address for a row that doesn’t exist in the dictionary.
Funny, hard coding addresses hits us back again 🙂 But the good news is that we can do something with it.

We will create another get_line_address function but this time in the driver. So all calls go to the original function and are passed to the driver for details.
With such approach, we can overwrite this function in WiFi driver and always return 0.
This change brings back problem with addressing line bigger than fourth. We need to overwrite function in the ILI driver.

   def init(self):
        """init function"""
        if self.initialized:
            return
        for address in range(self.height):
            self.address.append(100 + (address * self.width))
(...)
    def get_line_address(self, idx):
        return self.address[idx]

Case solved, let’s move on.

The second bug, with not clearing pixels when drawing a letter, is not complicated by an idea but require some work.
Why is it happening?
We have a matrix 8×8 pixels. Hex value describes what pixels are set. And we only look at enabled pixels ignoring empty spaces. And this is the problem 🙂
I think we do not have a function to clear a pixel 🙂 or better draw a pixel with a background colour.
It may sound simple, why not to draw a pixel with colour and background colour using the draw_pixel function. Good idea and it would work for page drawing but it is a bad idea for area drawing.
Why?
Because page drawing operates on the buffer and has only one colour but area works on whole selected area, in the case of draw_pixel it selects 1-pixel area and draw a pixel. We would waste a lot of bandwidth to set area and draw a pixel. We need a different approach, we need to draw on whole letter area with a proper colour. It is the performance wise approach.
So quick summary, for the page we will use pixel drawing, for area drawing we will write something with a performance.
But this requires quite an amount of changes. Why ?
For area drawing it is inconvenient to swap colour/background colour to paint a pixel. So we will add a colour parameter to draw_pixel.
We will delete functions: _converted_background_color and _converted_color. In their place we will have a function to convert any passed colour:

    @abc.abstractmethod
    def _convert_color(self, color):
        """convert color to avaible one"""
        pass

We have to fix all tests 🙂 and after a while, we are back to green.

We are ready to write the simplest and not effective draw_text:

    def draw_text(self, pos_x, pos_y, text, with_background=False):
        """draw a text"""
        font = self.options['font']
        idx = 0
        for letter in text:
            self._draw_letter(pos_x + idx, pos_y, letter, with_background)
            idx += font.size[0]

    def _draw_letter(self, pos_x, pos_y, letter, with_background=False):
        """draw a letter"""
        font = self.options['font']
        bits = font.size[0]
        for row, data in enumerate(font.get(letter)):
            for bit in range(bits):
                if data & 0x01:
                    self.draw_pixel(
                        pos_x + bit, pos_y + row, self.color
                    )
                elif with_background:
                    self.draw_pixel(
                        pos_x + bit, pos_y + row, self.background_color
                    )
                data >>= 1

Now, just for fun let’s look at performance. The demo ili9325_hd44780 without background:

real    0m4.072s
user    0m3.520s
sys     0m0.030s

and with the background painted via draw_pixel:

real    0m10.643s
user    0m10.090s
sys     0m0.030s

A huge difference. We will try and put some speed into this by overwriting _draw_letter when a background is set to True in Area drawing.

    def _draw_letter(self, pos_x, pos_y, letter, with_background=False):
        """draw a letter"""
        if not with_background:
            super()._draw_letter(pos_x, pos_y, letter, with_background)
        else:
            font = self.options['font']
            self._set_area(
                pos_x,
                pos_y,
                pos_x + font.size[0] - 1,
                pos_y + font.size[1] - 1
            )

            bits = font.size[0]
            color = self._convert_color(self.options['color'])
            background_color = self._convert_color(self.options['background_color'])
            for row, data in enumerate(font.get(letter)):
                for bit in range(bits):
                    if data & 0x01:
                        self.driver.data(color, None)
                    elif with_background:
                        self.driver.data(background_color, None)
                    data >>= 1   

This code gives us:

real    0m1.620s
user    0m1.060s
sys     0m0.040s

It’s good, isn’t it?

What now?
I need to make sure that everything works on NJU and SSD. RPi switch 🙂

And we have a little problem. If we want to see something we need to call flush(True) on GfxLCD. It is not necessary for ILI (because it works directly on display) but it is required on NJU and SSD (they work on the buffer). Good news is that we can use events and call flush on post_flush 🙂
So in the driver that changes graphical LCD into char LCD, we need to add:

class HD44780(BaseDriver, FlushEvent):
    def __init__(self, gfxlcd, lcd_flush=False):
        self.lcd_flush = lcd_flush
    (..)
    def pre_flush(self, buffer):
        pass

    def post_flush(self, buffer):
        """called after flush()"""
        if self.lcd_flush:
            self.gfxlcd.flush(True)

After that we need to do a proper initialization:

    lcd = NJU6450(122, 32, GPIO())

    drv = HD44780(lcd, True)

This True will call flush on LCD after buffer is moved from char to graphical display

    print(drv.width, drv.height)
    lcd = CharLCD(drv.width, drv.height, drv, 0, 0)
    lcd.init()
    lcd.write('First')

    lcd.write('HD44780', 6, 3)
    lcd.flush()
    lcd.write('/* ', 12, 0)
    lcd.write('|*|', 12, 1)
    lcd.write(' */', 12, 2)
    lcd.flush()

Yeh. It works on NJU and SSD. And both ILI displays I have.
What about compatibility with ProxyLCD?

import RPi.GPIO
from message_listener.server import Server
from iot_message.message import Message
from charlcd.handler import Handler
from charlcd.buffered import CharLCD
from gfxlcd.driver.nju6450.gpio import GPIO
from gfxlcd.driver.nju6450.nju6450 import NJU6450
from gfxlcd.driver.hd44780 import HD44780
RPi.GPIO.setmode(RPi.GPIO.BCM)

lcd_nju = NJU6450(122, 32, GPIO())

drv = HD44780(lcd_nju, True)
print(drv.width, drv.height)
lcd = CharLCD(drv.width, drv.height, drv, 0, 0)
lcd.init()

msg = Message('rpinju')

svr = Server(msg)
svr.add_handler('GfxLCD', Handler(lcd))
svr.run()

svr.join()

Config in app:

[lcd4]
name = NJU6450
size = 15x4
node_name = rpinju
stream = 1
type = charlcd

And success!

Stop! Jenkins time!

For long, long time I have skipped this part. But it is back and probably will take a long time:)
Let’s start from CharLCD package. Some new problems in old code 🙂 Seems pylint and flake8 are able to find more violations than some time ago but this is good.
Code refactored, uploaded to PyPi and merged to master
Next, GfxLCD.It has no develop branch, added. This library was made in hurry, we needed it for Doton project. It is time to clean it up.
I hit a huge wall. In some tests, I’m importing spidev and RPi.GPIO but they are not accessible on Jenkins machine. What to do?
Mock them! It was something new for me but works:

import sys
sys.path.append("../../")
from unittest.mock import patch, MagicMock

MockRPi = MagicMock()
MockSpidev = MagicMock()
modules = {
    "RPi": MockRPi,
    "RPi.GPIO": MockRPi.GPIO,
    "spidev": MockSpidev
}

patcher = patch.dict("sys.modules", modules)
patcher.start()

from gfxlcd.driver.ssd1306.spi import SPI
from gfxlcd.driver.ssd1306.ssd1306 import SSD1306


class TestNJU6450(object):
    def test_initialize(self):
        SSD1306(128, 64, SPI())

And now I see a huge amount of things to fix and refactor 🙂 But we cannot fix everything as we are using complicated inheritance.

Summary

Another day with displaying anything anywhere 🙂 This time we fixed problems with background and letters, addressing a proper position and flush on SSD and NJU chipsets.
Now all of our supported chipsets can work as the HD44780 display. And they can work with ProxyLCD 🙂

Don’t you think that we can format data on display in the better way? Stay tuned 🙂

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