Drive a 16x2 LCD display tutorial code update. Comments welcome

EL Wire/Tape/Panels, LEDs, pixels and strips, LCDs and TFTs, etc products from Adafruit

Moderators: adafruit_support_bill, adafruit

Please be positive and constructive with your questions and comments.
Locked
User avatar
jseyfert3
 
Posts: 40
Joined: Mon May 02, 2011 11:48 pm

Drive a 16x2 LCD display tutorial code update. Comments welcome

Post by jseyfert3 »

Hey all. First post here. Getting back into microcontrollers and Pi's (again). I dabbled with Arduinos on and off since I was introduced by a teacher in college 14 years ago. Bought a Pi Zero W in early 2020, fiddled very briefly with it, and it sat since then.

Recently dug my supplies out as I am going to build temp/humidity/air quality/weather remote sensors and have them feed back to a Raspberry Pi base station, which will hopefully eventually have a web server for accessing current and past data in numerical and graph form.

Anyway, after getting a button to blink some LEDs, I figured next step was pull out a trusty 16x2 character LCD and figure out how to do that with a Pi instead of Arduino. Used the Drive a 16x2 LCD with the Raspberry Pi tutorial as my starting point. And I ran into a couple of issues. I think I've fixed these, but looking for feedback on my fixes.

Firstly, the original code worked fine, just running the code manually. Got the display showing Date/Time/IP address by running the script. Original tutorial code here, for reference:

Code: Select all

# SPDX-FileCopyrightText: 2018 Mikey Sklar for Adafruit Industries
#
# SPDX-License-Identifier: MIT

from subprocess import Popen, PIPE
from time import sleep
from datetime import datetime
import board
import digitalio
import adafruit_character_lcd.character_lcd as characterlcd

# Modify this if you have a different sized character LCD
lcd_columns = 16
lcd_rows = 2

# compatible with all versions of RPI as of Jan. 2019
# v1 - v3B+
lcd_rs = digitalio.DigitalInOut(board.D22)
lcd_en = digitalio.DigitalInOut(board.D17)
lcd_d4 = digitalio.DigitalInOut(board.D25)
lcd_d5 = digitalio.DigitalInOut(board.D24)
lcd_d6 = digitalio.DigitalInOut(board.D23)
lcd_d7 = digitalio.DigitalInOut(board.D18)


# Initialise the lcd class
lcd = characterlcd.Character_LCD_Mono(lcd_rs, lcd_en, lcd_d4, lcd_d5, lcd_d6,
                                      lcd_d7, lcd_columns, lcd_rows)

# looking for an active Ethernet or WiFi device
def find_interface():
    find_device = "ip addr show"
    interface_parse = run_cmd(find_device)
    for line in interface_parse.splitlines():
        if "state UP" in line:
            dev_name = line.split(':')[1]
    return dev_name

# find an active IP on the first LIVE network device
def parse_ip():
    find_ip = "ip addr show %s" % interface
    find_ip = "ip addr show %s" % interface
    ip_parse = run_cmd(find_ip)
    for line in ip_parse.splitlines():
        if "inet " in line:
            ip = line.split(' ')[5]
            ip = ip.split('/')[0]
    return ip

# run unix shell command, return as ASCII
def run_cmd(cmd):
    p = Popen(cmd, shell=True, stdout=PIPE)
    output = p.communicate()[0]
    return output.decode('ascii')

# wipe LCD screen before we start
lcd.clear()

# before we start the main loop - detect active network device and ip address
sleep(2)
interface = find_interface()
ip_address = parse_ip()

while True:

    # date and time
    lcd_line_1 = datetime.now().strftime('%b %d  %H:%M:%S\n')

    # current ip address
    lcd_line_2 = "IP " + ip_address

    # combine both lines into one update to the display
    lcd.message = lcd_line_1 + lcd_line_2

    sleep(2)
The issues started when I tried to follow the next step, to Display Time & IP on Every Boot. For that, the following was given to make a systemd service.service file:

Code: Select all

[Unit]
Description=LCD date|time|ip
Requires=network-online.target
After=network-online.target

[Service]
ExecStart=/usr/bin/python3 Drive_a_16x2_LCD_with_the_Raspberry_Pi.py
WorkingDirectory=/home/pi
StandardOutput=inherit
StandardError=inherit
Restart=always
User=pi

[Install]
WantedBy=network-online.target
Seemed straightforward enough. But after modifying it for file name/working directory/user, I could start it and it worked when I ran

Code: Select all

systemctl start lcd.service
but if I enabled it

Code: Select all

systemctl enable lcd.service
and rebooted, it would not run. Instead, it would show the status as "Inactive (dead)".

There started an hour of Googling, before I realized most people used

Code: Select all

After=multi-user.target

WantedBy=multi-user.target
So I changed it to that, rebooted, and hooray! The service launches and the LCD displays!

But now I wanted to know what all this .target business was. So, 4 hours of reading systemd documentation resulted. Among lots of other useful information, I found the following statement, in regards to network-online.target (emphasis mine):
It is strongly recommended not to pull in this target too liberally: for example network server software should generally not pull this in (since server software generally is happy to accept local connections even before any routable network interface is up), its primary purpose is network client software that cannot operate without network.
I found other references to using Required=network-online.target delaying boot, as well as general references to using Required= causing services to fail to launch if the Required= target failed to launch. Additionally I learned that you don't have to use Required= or After= at all, in many cases simply a "WantedBy=" in the [Install] section is sufficient to launch your service, if it doesn't rely on any other services.

So, I changed the .service file to (where <username> is replaced by my username):

Code: Select all

[Unit]
Description=LCD date|time|ip

[Service]
ExecStart=/usr/bin/python3 LCD_date_time_ip.py
WorkingDirectory=/home/<username>/
StandardOutput=inherit
StandardError=inherit
Restart=always
User=<username>

[Install]
WantedBy=multi-user.target
This worked well. It's not going to get hung up if something happens to network-online.target, and starts running somewhere around the time a user is able to log in.

But then I went back and looked at the original python script. After all, the lcd.service file was requiring network-online.target. Why? And I realized the code was written with the assumption there WOULD be an IP address assigned. Sure enough, if I shut off my WiFi router, or booted when WiFi was unavailable, not only did the LCD not display anything, but looking at the system logs the script was failing, then constantly being restarted because the lcd.service file says Restart=always.

Code crashing is never desired, and obviously you want some checks in place to avoid your code crashing when common things happen. Not being connected to WiFi or having an IP can happen for many reasons. So I went through the code and while keeping the main structure, I changed it for two reasons:
  1. To not crash if an IP address was not available, and subsequently display on the LCD "IP not assigned" if no network device was in "state UP" or "IP pending" if a network device was in "state UP" but no IPv4 address was available (after connection to network but before DHCP process has finished)
  2. Changed sleep(2) to sleep(1) in the main while True loop to update clock once per second, since seconds are displayed on LCD, and added a separate timer to check for and update the IP address at a slower rate. This allows any DHCP updates, or loss of IP, to be displayed. Originally the IP adderess was only checked once on power-up.
So my code for the LCD tutorial now looks like this:

Code: Select all

# SPDX-FileCopyrightText: 2018 Mikey Sklar for Adafruit Industries
#
# SPDX-License-Identifier: MIT
# Modified by Jonathan Seyfert, 2022-01-22, to keep code from crashing when WiFi or IP is unavailable
 
from subprocess import Popen, PIPE
from time import sleep, perf_counter
from datetime import datetime
import board
import digitalio
import adafruit_character_lcd.character_lcd as characterlcd

# Modify this if you have a different sized character LCD
lcd_columns = 16
lcd_rows = 2

# compatible with all versions of RPI as of Jan. 2019
# v1 - v3B+
lcd_rs = digitalio.DigitalInOut(board.D22)
lcd_en = digitalio.DigitalInOut(board.D17)
lcd_d4 = digitalio.DigitalInOut(board.D25)
lcd_d5 = digitalio.DigitalInOut(board.D24)
lcd_d6 = digitalio.DigitalInOut(board.D23)
lcd_d7 = digitalio.DigitalInOut(board.D18)


# Initialise the lcd class
lcd = characterlcd.Character_LCD_Mono(lcd_rs, lcd_en, lcd_d4, lcd_d5, lcd_d6,
                                      lcd_d7, lcd_columns, lcd_rows)

# looking for an active Ethernet or WiFi device
def find_interface():
#    dev_name = 0 # sets dev_name so that function does not return Null and crash code
    find_device = "ip addr show"
    interface_parse = run_cmd(find_device)
    for line in interface_parse.splitlines():
        if "state UP" in line:
            dev_name = line.split(':')[1]
            return dev_name
    return 1 # avoids returning Null if "state UP" doesn't exist

# find an active IP on the first LIVE network device
def parse_ip():
    if interface == 1: # if true, no device is in "state UP", skip IP check
        return "not assigned " # display "IP not assigned" 
    ip = "0"
    find_ip = "ip addr show %s" % interface
    ip_parse = run_cmd(find_ip)
    for line in ip_parse.splitlines():
        if "inet " in line:
            ip = line.split(' ')[5]
            ip = ip.split('/')[0]
            return ip # returns IP address, if found
    return "pending      " # display "IP pending" when "state UP", but no IPv4 address yet

# run unix shell command, return as ASCII
def run_cmd(cmd):
    p = Popen(cmd, shell=True, stdout=PIPE)
    output = p.communicate()[0]
    return output.decode('ascii')

# wipe LCD screen before we start
lcd.clear()

# before we start the main loop - detect active network device and ip address
# set timer to = perf_counter(), for later use in IP update check
interface = find_interface()
ip_address = parse_ip()
timer = perf_counter()

while True:
    # check for new IP addresses, at a slower rate than updating the clock
    if perf_counter() - timer >= 15:
        interface = find_interface()
        ip_address = parse_ip()
        timer = perf_counter()

    # date and time
    lcd_line_1 = datetime.now().strftime('%b %d  %H:%M:%S\n')

    # current ip address
    lcd_line_2 = "IP " + ip_address

    # combine both lines into one update to the display
    lcd.message = lcd_line_1 + lcd_line_2

    sleep(1)
I've tested this by rebooting with WiFi, turning the Pi's WiFi off and back on while the Pi is running, rebooting and unplugging my WiFi router when the Pi is turning off, so that it boots and launches the code with no WiFi (and hence no IP address available), and also with unplugging my WiFi router with the Pi already booted and running the script. In all test cases, the script handles it with ease. It does not crash, it continues displaying the date/time whenever the Pi is running.

When their is no network connection in "state UP" aka WiFi off or unavailable, it displays "IP not assigned".

When their is a network connection in "state UP" but no IPv4 address is assigned, aka initial WiFi connection before DHCP assignment, it displays "IP pending".

When network connection is in "state UP" and IPv4 address is assigned, it displays "IP 192.168.1.115" (or presumably whatever IP is assigned, I haven't tried forcing my router to assign it a new DHCP address).

I do not mean anything negative to the original author. It worked for him, and in a lot of cases that's all you need. These are just some suggested improvements for increased utility and robustness.

Special thanks to my EE teacher at the Community College I went to, for both openly encouraging my exploration of microcontroller electronics and coding, and for the excellent example he gave me one time. I brought in an Arduino with some buttons and two 7 segment LED displays. It was a basic thermostat, programmable with the buttons to set turn-on and turn-off temps, and displayed current temp when not in programming mode. He immediately started mashing all the buttons super fast. I said "what are you doing?"

His reply was "I'm trying to break it." (the code, not the hardware)

It was my first, and best lesson that when coding, you need to think about what could happen, not just the intended usage. It was a brilliantly simple lesson, and part of the reason it took just a quick glance at the original tutorial code to realize it could not handle not having a network connection.

Any questions or comments welcome. I'm here to learn. I know there are many improvements to the improvements I've already done that could still be done to this code.

User avatar
adafruit_support_mike
 
Posts: 67454
Joined: Thu Feb 11, 2010 2:51 pm

Re: Drive a 16x2 LCD display tutorial code update. Comments welcome

Post by adafruit_support_mike »

That looks like a good adaptation, thanks for posting it!
jseyfert3 wrote: Sun Jan 22, 2023 6:46 pm I do not mean anything negative to the original author. It worked for him, and in a lot of cases that's all you need. These are just some suggested improvements for increased utility and robustness.
No worries.. in general, we write proof-of-concept code. Our goal is to show how to interact with a device in an understandable way.

Mature production code is 10% main-line functionality and 90% error handling. And as a rule, error-handling code isn't interesting to write or to read.. it's all "if this condition isn't met, respond in some sensible way". Arranging code to interact smoothly with error handling can be an interesting challenge, but is still far removed from what people tend to consider 'the good stuff'. It also tends to make the main-line code more abstract and a bit harder to understand.

You've done an excellent job of adding error handling to the existing script. In real-world terms, you deserve praise for noticing there was an error to handle and doing something about it. For the specific context of our tutorials, we'll have to make sure the error handling doesn't steal focus from the 'this is how you use the LCD' part.


You also have my congratulations/sympathy for successfully navigating the documentation for systemd. I rank myself among those who dislike it.

The only people I've ever seen on record are network administrators trying to build server farms with virtual machines in Docker containers that need to communicate with a back end that has to be able to fail and restart automatically, etc, etc, etc. And I freely admit that systemd does make extremely difficult things possible.

From the embedded perspective, the complexity systemd allows comes at the cost of making simple things difficult. A thermostat or IP address tracker shouldn't need an experienced system administrator to launch when the machine boots.

User avatar
jseyfert3
 
Posts: 40
Joined: Mon May 02, 2011 11:48 pm

Re: Drive a 16x2 LCD display tutorial code update. Comments welcome

Post by jseyfert3 »

I agree, everything you said makes sense. Though I would say for controllers like the Pi that run an OS, some very basic level of system administration is going to be required. An Arduino runs the code you put on it and nothing else, every time you power it up, without fail. While a Pi runs the code you put on it, but that code must be launched with and interact successfully with the OS.

Of course it depends exactly on what your goals are. Eventually I want a Pi as a base station for remote weather sensors. With a small webserver so I can use my phone to access current data and historical graphs, and log all received data. So I want stuff written so the Pi can run constantly, watch for and ensure my script(s) are running, handle reboots/power loss/loss of WiFi/etc. Maybe a simple email server too to send email alerts for things like "remote sensor X needs to be charged soon".
adafruit_support_mike wrote: Mon Jan 23, 2023 2:11 am You also have my congratulations/sympathy for successfully navigating the documentation for systemd. I rank myself among those who dislike it.
Thanks. Though part of that is just me. I have the ability, at times, to wade deep through the details of boring stuff most people want to leave alone. My manager asked if he could make shirts for my team that on the front said "WWJD", underneath in small print "What would Jon do?", and on the back the same in larger text, over an amusing picture of me someone took once, and below that picture it says "Did you read the manual?" I always seem to me the one at work that's like "hey, this thing that doesn't work? The manual says X." Lol

Anyway, I don't want to take up too much of your time, but I always like sharing my projects. I found the Adafruit Learning System User Page Editor. Do things posted here show up in the tutorials section? And if so, do you have information to refer me to for guidelines? I found one 50 page PDF yesterday, that started with "See this link first" but that link resulted in a 404 error.

Can't wait to get my project parts. I ordered a bunch of air temp/humility/particulate/VOC sensors. Plan to have remote sensors with the 32u4 Feather boards containing the RFM69 915 MHz radios, feeding the OLED RFM69 Pi tophat, running a webserver to allow me to view data and graphs from my phone, and probably with an email server too so I can get alerts like "sensor X needs to be charged" or "sensor Y has not sent any data in Y hours".

User avatar
adafruit_support_mike
 
Posts: 67454
Joined: Thu Feb 11, 2010 2:51 pm

Re: Drive a 16x2 LCD display tutorial code update. Comments welcome

Post by adafruit_support_mike »

jseyfert3 wrote: Mon Jan 23, 2023 3:16 pm I agree, everything you said makes sense. Though I would say for controllers like the Pi that run an OS, some very basic level of system administration is going to be required.
I'm thinking of applications like an automated test fixture for third-party board assembly.

Assembly houses do their best, but their ability to deliver boards that work is limited by the information a client gives them. If you give them a fixture that runs a series of tests and provides sensible descriptions of any errors it finds, they can do a much better job of finding and fixing trouble spots in their assembly process. Getting an assembled board to pass the automated test becomes part of their setup tooling process.

The RasPi is an excellent platform for that kind of system, but you want it to boot to the test code without any fuss. At worst, you want the assembly house staff to swap in a new copy of the SD card if the original one gets corrupted.
jseyfert3 wrote: Mon Jan 23, 2023 3:16 pm Do things posted here show up in the tutorials section?
No, the forums and the Learning System are separate bodies of information. Ladyada and a couple of other people invite non-staff writers to do tutorials occasionally.

That said, I'll mention your code to the author of the tutorial you mentioned. If it looks like a good addition, they might work it in.

User avatar
jseyfert3
 
Posts: 40
Joined: Mon May 02, 2011 11:48 pm

Re: Drive a 16x2 LCD display tutorial code update. Comments welcome

Post by jseyfert3 »

adafruit_support_mike wrote: Tue Jan 24, 2023 1:25 am
jseyfert3 wrote: Mon Jan 23, 2023 3:16 pm Do things posted here show up in the tutorials section?
No, the forums and the Learning System are separate bodies of information. Ladyada and a couple of other people invite non-staff writers to do tutorials occasionally.

That said, I'll mention your code to the author of the tutorial you mentioned. If it looks like a good addition, they might work it in.
Sorry, I was referring to the Adafruit Learning System, not the forums. If I go to that, there's a "User Pages" tab with my name, and the ability to make pages. And it says:
Welcome to the Adafruit Learning System User Page Editor

Here you can create personal pages to share. To get started, click the "New Blank Page" button. Any pages you create will automatically be available for the world to see. Once you create your first page, the editor elements toolbar will appear. Then you can select an element such as text, image, code, etc., to add to your page.
Specifically, it says "Any pages you create will automatically be available for the world to see." So where do those pages show up for the world to see?

User avatar
adafruit_support_mike
 
Posts: 67454
Joined: Thu Feb 11, 2010 2:51 pm

Re: Drive a 16x2 LCD display tutorial code update. Comments welcome

Post by adafruit_support_mike »

Hmm.. that's a new feature. It looks like you can post information for public use.

User avatar
mikeysklar
 
Posts: 13936
Joined: Mon Aug 01, 2016 8:10 pm

Re: Drive a 16x2 LCD display tutorial code update. Comments welcome

Post by mikeysklar »

@jseyfert3,

I'm the original 16x2 LCD Pi guide author. Thank you for taking the time to submit you suggestions for the initialization file and non-IP address support. I wired up a 16x2 LCD and tested the changes on my Pi4b with no issue running Bullseye 32-bit desktop.

I've gone ahead and created a PR with the code changes. It still needs to be approved, but you should see them go public in the next day or so.

https://github.com/adafruit/Adafruit_Le ... /pull/2388

Locked
Please be positive and constructive with your questions and comments.

Return to “Glowy things (LCD, LED, TFT, EL) purchased at Adafruit”