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 π
One comment