NodeMCU LCD connected to CharLCD package – part 1: a whaaat ?

This time my two projects, CharLCD and NodeMCU boilerplate join forces in endless effort to display something somewhere πŸ™‚

What does it mean? It means that we will add another driver to CharLCD. This will allow us to use remote LCD with Raspberry Pi and later with any application in Python. It should be possible to use it with LCD Manager package and this is something very interesting πŸ™‚

As a remote LCD we will use HD44780 (40×4) hooked to NodeMCU via i2c (but we know that we can plug any LCD via gpio or i2c πŸ˜› ) and it will listen to broadcast messages.

Rpi (with CharLCD and driver) will broadcast UDP messages into network.

Lets get started.


We have three parts, driver that we need to add to CharLCD, server at NodeMCU and library for Raspberry Pi to use our protocol iot:1. Why library is separated from CharLCD? Because it will allow us to use it in any platform (like use screen via app from desktop computer) and any other project.
So this is our first focus, python implementation of network_message.lua


Our funny iot:1 protocol uses UDP broadcast messages to communicate between devices. It is nothing more that a json string under the hood.
We already have a module for NodeMCU called network_message but nothing for Python.
Lets look quickly at a structure on sample message:

    "protocol": "iot:1",
    "node": "Rpi-lcd-1",
    "chip_id": "RpiB",
    "event": "display",
    "parameters: [
        "content": "-(=^.^)"
    "response": "",
    "targets": [

It was almost good for our case. What was missing was the ability to send some parameters like what to display πŸ˜› . That’s why I added field parameters. I’m almost sure that it will change but for now it is enough.

Pythonic IOT_Message

We have a Message class. It takes two arguments, one for node name second for chip_id. Second is not mandatory if left empty app will try and get some unique and permanent id. In Linux its done via:

cat /var/lib/dbus/machine-id

and under Windows via:

wmic csproduct get uuid.

Function prepare_message may take a dict variable as a parameter. If so it will replace keys with those values and return filled message.
Function decode_message checks if protocol is correct and if current node is in targets or targets contains special keyword ALL.

Library is not available on PyPi for now, so if you want it install via:

pip install git+

CharLCD WiFiLCD driver

We have some more work to do. Let think how this driver should work and what each function should do.
We have two options, one if any function is called, send message via network to node – work as proxy. It would generate a nice traffic but require less changes.
Any other option like using write to store data in buffer and send full message require nice amount of work. Ideally speaking when using direct mode driver should send each command at once. And in buffered mode driver should gather what needs to be displayed and send it on flush. But there is no flush in driver :).
Another problem is how to send messages. If we have only one application that uses port 5053 then there is no problem. In other cases hmm not good:) Ideally we should have a nice Message Broker and it would sends messages (and receive) and driver connect to it via shared memory/network/ other means. And other apps just use it ignoring whole network stuff. Why? Because apps don’t care about network πŸ™‚ they care about preparing message. And imagine that each app needs to use network.. so much duplicated code.
That’s two big problems at once.
For now lets ignore second problem and focus on first.

I’m thinking about a mixed option. WiFi driver would work in one of two modes, direct or buffered. It is similar to CharLCD option. Or even better lets write two drivers πŸ™‚ WiFi direct and Wifi buffered. In direct mode it will send message with event to lcd like move cursor, display char and so.
In buffered mode it would use write to prepare buffer but how to call send from CharLCD’s flush ? I have an idea.. we will use event functions. Two events one at the start of flush and second after flush. This way when we hook to after flush we may send message to remote lcd.
Looking at this I’m pretty sure that direct is more or less useless. But still nice thing to have πŸ˜›

WiFi direct driver

Ok lets start from this one. It should be much easier in implementation that buffered. We need to change functions to send UDP broadcast and not write to any GPIOs.
Our interface for drivers force us to implement few functions: init, cmd, shutdown, send, write, char, set_mode and __init__.
Lets start from constructor. It needs to know where (node name) we want to send and how (Message class). Hmm maybe even names? Yes that’s better, names so a list. And of course tuple with address is necessary.
Function init should initialize socket.
Now cmd, write, char, they are rather simple, each of them will prepare message and store it in buffer. Calling send will read data from buffer and broadcast message into network. We will use events: lcd.cmd, lcd.write and lcd.char.
And first run first problems πŸ™‚
CharLCD checks dictionary with pins for E2 value but we have no pins variable. So I added a dummy. And it worked.
We know that driver can send some messages but we have nothing to receive it. Time to change this.


How to begin? From creating a new shiny file lcd_44780_server.lua.
What we want to do there is to start UDP server and act according to received events. See that direct approach works directly on hardware. So each of hardware command is transmitted to lcd and executed there. Huge bandwidth overuse and totally useless approach. But so much fun πŸ˜€
And it stays with ideology (almost): each mode must work on any driver.
From what I know only two events are needed: lcd.cmd and lcd.char. So with all this info we can write a script:

local server = {}
server.lcd = nil

network_message = require "network_message"

server.svr = net.createServer(net.UDP)
server.svr:on('receive', function(socket, message)
    message = network_message.decodeMessage(message)
    if message ~= nil and message.event ~= nil then
        print(message.event..", "", "..message.parameters.enable)
        if message.event == 'lcd.cmd' then
                message.parameters.enable + 1
        if message.event == 'lcd.char' then
                message.parameters.enable + 1



return server

Why we are increasing enable by 1? Because CharLCD works at 0 and 1 but in lua we have 1 and 2.

And new main.lua:

print ("core ready")

drv = require("lcd_hd44780_i2c")
lcd = require("lcd_hd44780")

drv.addr = 0x20
drv.pins = {
    RS= 4,
    E1= 5,
    E2= 6,
    DB4= 0,
    DB5= 1,
    DB6= 2,
    DB7= 3,

lcd.lcd(drv, 40, 4)
lcd.cursor_visible = 0

svr_lcd = require "lcd_hd44780_server"
svr_lcd.lcd = lcd

In main.lua we initialize screen,Β  driver and attach lcd to server. This implementation is quite bad because after require server is started but has no lcd. So with good timing it will crash. But do not worry for now.

We need something to test it. So back to Python and CharLCD.

Tests and demos

In CharLCD package we have a nice demo directory. Lets add another file there:

# -*- coding: utf-8 -*-
"""test script for wifi-direct lcd input"""
import sys
from import WiFi  # NOQA
from charlcd import direct as charlcd  # NOQA
from iot_message import message
import time

def test1():
    msg = message.Message('node-40x4')
    drv = WiFi(msg, ['node-40x4'], ('', 5053))
    lcd = charlcd.CharLCD(40, 4, drv)
    lcd.set_xy(10, 2)
    lcd.write('-Second blarg !')
    lcd.set_xy(10, 1)
    lcd.write("-second line")


To my surprise only one thing I had to change. Function send in driver now has a time.sleep(0.05) delay after message send. Without it quite a mess happens. And with it we have additional delay. And that was all.
See that calling lcd.init() will transfer initialization commands to remote lcd. So it will be initialized twice πŸ™‚
But it works! And this is the biggest surprise!

Another tests:

def test2():
    msg = message.Message('node-40x4')
    drv = WiFi(msg, ['node-40x4'], ('', 5053))
    lcd = charlcd.CharLCD(40, 4, drv)

def test3():
    """demo 3 - lcd 40x4 by gpio"""
    msg = message.Message('node-40x4')
    drv = WiFi(msg, ['node-40x4'], ('', 5053))
    lcd = charlcd.CharLCD(40, 4, drv)
    lcd.write('-First blarg1 !')
    lcd.write('-Second blarg2 !', 0, 1)
    lcd.write('-Third blarg3 !', 0, 2)
    lcd.write('-Fourth blarg4 !', 0, 3)
    lcd.write('12345678901234567890', 15, 1)'1234567890qwertyuiopasdfghjkl')

I just took tests from other file and replaced driver. This is exactly how it should be!

Stop! Jenkins time!

And another problem.. who stole my VM with Jenkins? O.o. It is definitely time to move to docker..


Now I’m sure you know why we wrote wifi direct (each mode on each drv or something πŸ™‚ ) and you know why do not use it πŸ™‚ And one more info, this is currently not on PyPi only on Bitbucket @ remote_driver branch.
In next part we will do what really have some use-value, buffered driver. And finally some proxy to choose best wifi driver according to lcd mode.
If everything goes correctly we will do a final test, Piader on remote lcd – pure madness.


Leave a Reply

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

You are commenting using your 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