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)
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
Code: Select all
systemctl start lcd.service
Code: Select all
systemctl enable lcd.service
There started an hour of Googling, before I realized most people used
Code: Select all
After=multi-user.target
WantedBy=multi-user.target
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):
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.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.
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
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:
- 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)
- 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.
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)
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.