RemoteLCD Node on Raspberry Pi with ILI9325 – HD44780 simulation

We have a remote LCD Node that works on NodeMCU board. It has a 40×4 char LCD attached to it and displays text received via a network message. We also can use Raspberry Pi as a remote LCD. But all those LCDs are small. We need something bigger and we have it! This something is an ILI9325 graphical display. There is a huge difference between char and graphic but we can simulate char interface. Our GfxLCD library has an ability to display a text and with a little bit of tinkering, it should work :D.
This could bring nice results, imagine a dump from Symfony with multiline formatting thanks to the ProxyLCD project. Nice ๐Ÿ™‚

Planning

So what’s the Plan?
First quick remember how to use RPi as a remote LCD with CharLCD lib.
Next, we need to write a handler for GfxLCD module. This handler needs to be compatible with CharLCD one.
Or maybe we will create a class that implements LCD and buffered interface from CharLCD? With this, we could use ILI or any other graphical LCD with CharLCD ๐Ÿ™‚ crazy but may work!
Or the best way! Write a new driver that implements BaseDriver from CharLCD module!
With this we just replace driver and it must work:)
What about the text?
We need to measure a maximum number of chars in width and height. This is a job for a new driver. What else? We will see ๐Ÿ™‚

CharLCD and remote LCD

This is for reference. If we want to use RPi as a remote LCD node we need to install:

pip install message_listener
pip install iot_message
pip install charlcd

With this, code for node looks like this:

from message_listener.server import Server
from charlcd.drivers.i2c import I2C
from charlcd import buffered as lcd
from iot_message.message import Message
from charlcd.handler import Handler

i2c_20x4 = I2C(0x3b, 1)
i2c_20x4.pins = {
    'RS': 6,
    'E': 4,
    'E2': None,
    'DB4': 0,
    'DB5': 1,
    'DB6': 2,
    'DB7': 3
}

lcd = lcd.CharLCD(20, 4, i2c_20x4)
lcd.init()

msg = Message('rpi1')

svr = Server(msg)
svr.add_handler('20x4', Handler(lcd))

svr.run()

svr.join()

In such simple way, we turn RPi in remote LCD node. What we want now is something similar for GfxLCD.

HD44780 driver in GfxLCD – preparation

The most important part is to have functions like: cmd, send, write, char. So we need to implement them.
What about driver initialization?
We will do a small cheat:) As a parameter to the driver, we will pass a nice and ready GfxLCD instance.

So we will start with tests:

import sys
from nose.tools import assert_equal
sys.path.append("../../")
from gfxlcd.driver.null.null_page import NullPage
from gfxlcd.driver.hd44780 import HD44780
from charlcd.buffered import CharLCD


class TestChip(object):
    def setUp(self):
        self.gfx_lcd = NullPage(132, 16, None, False)
        self.drv = HD44780(self.gfx_lcd)

    def test_get_size_in_chars(self):
        assert_equal(16, self.drv.width)
        assert_equal(2, self.drv.height)

And code to fulfil these tests:

from charlcd.drivers.base import BaseDriver


class HD44780(BaseDriver):
    def __init__(self, gfxlcd):
        """Class init"""
        self.gfxlcd = gfxlcd
        self.mode = 0
        self.initialized = False
        self.font = self.gfxlcd.options['font']
        self.width = self.gfxlcd.width // self.font.size[0]
        self.height = self.gfxlcd.height // self.font.size[1]
        self.pins = {
            'E2': None
        }
        self.position = {
            'x': 0,
            'y': 0
        }

    def init(self):
        """init function"""
        if self.initialized:
            return
        self.gfxlcd.init()
        self.initialized = True

Size in characters is calculated from screen size and font size. We also need to define pin E2 because it is hard coded in CharLCD ๐Ÿ˜€
Why we have a position? It’s required to translate row/column to a pixel position.

There is one big problem ahead, we have hard coded line addresses for four lines (I didn’t meet a char LCD with more that 4 rows) but with GfxLCD we can have more.

But this is for later. Now, let’s can add a test for driver initialization:

    def test_init_small_hd44780(self):
        lcd = CharLCD(self.drv.width, self.drv.height, self.drv, 0, 0)
        lcd.init()

And we are in nice bright red ๐Ÿ™‚

To return to green we need to write a cmd function but this is not easy. This function initializes and sets options on display, so it is tightly coupled with hardware.
Looking at command codes I think we can safely ignore all command lower that 0x80. Why?
Because we need only commands to set cursor position. And they start from 0x80.
With:

   def cmd(self, char, enable=0):
        """write command"""
        if char < 0x80:
            return
        
        raise NotImplementedError("cmd not implemented")

in our new driver, we are back to green.

Another test, this one tests if we can write to buffer without any exceptions:

    def test_write_to_buffer(self):
        lcd = self.get_lcd()
        lcd.write('Hello')
        lcd.write('     world', 0, 1)
        self.output[0] = "Hello" + " ".ljust(11, " ")
        self.output[1] = "     world" + " ".ljust(6, " ")
        assert_equal(lcd.buffer, self.output)

Yes, we can!

And what will happens if we call the flush function?

    def test_flush(self):
        lcd = self.get_lcd()
        lcd.write('Hello')
        lcd.write('     world', 0, 1)
        lcd.flush()

as expected it fails. Going back to green is simple, we need to change char function to:


    def char(self, char, enable=0):
        """write char to lcd"""
        self.gfxlcd.draw_text(0, 0, char)

But this is only green in the test. A glimpse at the code showsย that it will fail in the real example.
But we know that we are ready to take the challenge and fix all problems.

HD44780 driver in GfxLCD – implementation

Let’s start with a demo:

import sys
sys.path.append("../../")
import RPi.GPIO as GPIO  # NOQA pylint: disable=I0011,F0401
from charlcd.buffered import CharLCD # NOQA
from gfxlcd.driver.ili9325.gpio import GPIO as ILIGPIO
from gfxlcd.driver.ili9325.ili9325 import ILI9325
from gfxlcd.driver.hd44780 import HD44780

GPIO.setmode(GPIO.BCM)


def test1():
    """demo """
    ili_drv = ILIGPIO()
    ili_drv.pins['LED'] = 6
    ili_drv.pins['CS'] = 18
    lcd = ILI9325(240, 320, ili_drv)
    lcd.auto_flush = False
    lcd.rotation = 0

    drv = HD44780(lcd)
    lcd = CharLCD(drv.width, drv.height, drv, 0, 0)
    lcd.init()
    
    lcd.write('-!Second blarg!')
    lcd.write("-second line", 0, 1)
    lcd.flush()


test1()

With the code we have the only visible effect is a mess in the top-left corner. If we add change position after each char is drawn we should see some more sense on the screen:

    def char(self, char, enable=0):
        """write char to lcd"""
        self.gfxlcd.draw_text(
            self.position['x'], self.position['y'], char
        )
        self._increase_x()

    def _increase_x(self):
        self.position['x'] += self.font.size[0]

It is one line instead of two lines. Let’s try and fix this.

    def cmd(self, char, enable=0):
        """write command"""
        if char < 0x80: return if char >= 0xD4:
            self.position = {
                'x': (char - 0xD4) * self.font.size[1],
                'y': 3 * self.font.size[1]
            }
        elif char >= 0x94 and char < 0xC0: self.position = { 'x': (char - 0x94) * self.font.size[1], 'y': 2 * self.font.size[1] } elif char >= 0xC0 and char < 0xD4:
            self.position = {
                'x': (char - 0xC0) * self.font.size[1],
                'y': self.font.size[1]
            }
        else:
            self.position = {
                'x': (char - 0x80) * self.font.size[1],
                'y': 0
            }

And expand our demo a little bit:

    lcd.write('/* ', 19, 0)
    lcd.write('|*|', 19, 1)
    lcd.write(' */', 19, 2)
    lcd.flush()

What have we here:

It works!

But if we add

    lcd.write('BUM', 19, 5)
    lcd.flush()

we get:

(..)
    return LCD_LINES[pos_y]
IndexError: list index out of range

Ehh.. hardcoding line addresses hit us back. There is no easy way to change it or is it?
Let’s overwrite addresses in HD44780 class!

from charlcd.abstract import lcd as char_lcd
(..)
    def init(self):
        """init function"""
        if self.initialized:
            return
        char_lcd.LCD_LINES = []
        for address in range(self.height):
            char_lcd.LCD_LINES.append(100 + (address * self.width))

        self.gfxlcd.init()
        self.initialized = True
(..)
    def cmd(self, char, enable=0):
        """write command - set cursor position"""
        if char < 100:
            return
        char -= 100
        y = char // self.width
        x = char - (y*self.width)
        self.position = {
            'x': x * self.font.size[0],
            'y': y * self.font.size[1]
        }

In init function, we overwrite LCD_LINES array. It keeps starting address of each row. Start + offset gives us a column in this row. What we are doing in the code is to create starting addresses with fixed length. This way we can easily operate on our GfxLCD buffer.

And… Yes! Our demo works!

Summary

Why we are doing something so strange as emulation HD44780 char LCD on graphical LCD?
Because we can ๐Ÿ™‚ And it should go nicely with the ProxyLCD project ๐Ÿ™‚
During tests, I found out two bugs. First, we cannot set full size due to hard coded packet limit, and second, we are not painting background colour, so letters overlaps and a space do not clear an area.
Bug with size is in CharLCD and bug with a background is in GfxLCD.

But the main goal isย done, we can use ILI9325 as an HD44780 ๐Ÿ™‚ with CharLCD package.

Advertisements

2 comments

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