last updated: 2021-04-19
My Mechanical Ventilation Heat Recovery (MVHR) system from Paul was updated with a Teensy and a Raspberry Pi (see Piventi and is now sending MQTT messages:
To see this messages and manipulate the air flow I needed a touch panel. I bought a 3.5" PiTFT from Adafruit and use it with a Raspberry Pi 3. The Pi is connected over WiFi an is also getting data from a weather station.
To draw the graphical screen and use the touch function of the screen I use Kivy on the console (without X).
The panel worked great from 2017 on. After an update from stretch to Buster (2020) I first couldn't get kivy working and had to flash a new SD card. After long hours I got kivy working in headless mode as described here. But then I saw that the touchscreen didn't work as expected. After other long hours digging the internet I came to the conclusion that there is no chance to get my resistive touchscreen work in Buster. So I decided to revert to stretch as I saw on the adafruit page: "The last known for-sure tested-and-working version is March 13, 2018". Aaargh! Again it didn't' work, so back to Buster and more digging.
Now here is the procedure:
Flash an SD card with Raspi OS lite (headless) image and add an empty file called ssh
to the boot partition. And also a file called wpa_supplicant.conf
with the following content and your WiFi settings:
country=LU
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
network={
ssid="***"
psk="***"
}
As I like fix IP addresses and don't want to search with nmap, I give the Raspi a fix IP by mounting rootfs and adding the following lines to /etc/dhcpcd.conf
interface wlan0
static ip_address=192.168.1.222
static routers=192.168.1.1
static domain_name_servers=192.168.1.1
Reboot and ssh into the Raspi (pw: raspberry), update the files, change the password with passwd
and install the midnight commander mc
and htop
for maintenance:
sudo ssh pi@192.168.1.222
//sudo apt update
//sudo apt upgrade
passwd
sudo apt install mc htop
I read that in order to launch Kivy from the console you need to compile SDL2 from source, as the one bundled with Buster is not compiled with the right back-end. Here are the commands to install everything:
sudo apt-get install libfreetype6-dev libgl1-mesa-dev libgles2-mesa-dev \
libdrm-dev libgbm-dev libudev-dev libasound2-dev liblzma-dev libjpeg-dev \
libtiff-dev libwebp-dev git build-essential
sudo apt-get install gir1.2-ibus-1.0 libdbus-1-dev libegl1-mesa-dev \
libibus-1.0-5 libibus-1.0-dev libice-dev libsm-dev libsndio-dev \
libwayland-bin libwayland-dev libxi-dev libxinerama-dev libxkbcommon-dev \
libxrandr-dev libxss-dev libxt-dev libxv-dev x11proto-randr-dev \
x11proto-scrnsaver-dev x11proto-video-dev x11proto-xinerama-dev
wget https://libsdl.org/release/SDL2-2.0.10.tar.gz
wget https://libsdl.org/projects/SDL_image/release/SDL2_image-2.0.5.tar.gz
wget https://libsdl.org/projects/SDL_mixer/release/SDL2_mixer-2.0.4.tar.gz
wget https://libsdl.org/projects/SDL_ttf/release/SDL2_ttf-2.0.15.tar.gz
tar -zxvf SDL2-2.0.10.tar.gz && tar -zxvf SDL2_image-2.0.5.tar.gz
tar -zxvf SDL2_mixer-2.0.4.tar.gz && tar -zxvf SDL2_ttf-2.0.15.tar.gz
pushd SDL2-2.0.10
./configure --enable-video-kmsdrm --disable-video-opengl --disable-video-x11 --disable-video-rpi
make -j$(nproc)
sudo make install
popd
pushd SDL2_image-2.0.5
./configure
make -j$(nproc)
sudo make install
popd
pushd SDL2_mixer-2.0.4
./configure
make -j$(nproc)
sudo make install
popd
pushd SDL2_ttf-2.0.15
./configure
make -j$(nproc)
sudo make install
popd
sudo apt install pkg-config libgl1-mesa-dev libgles2-mesa-dev \
python3-setuptools libgstreamer1.0-dev git-core \
gstreamer1.0-plugins-{bad,base,good,ugly} \
gstreamer1.0-{omx,alsa} python3-dev libmtdev-dev \
xclip xsel libjpeg-dev
sudo apt install python3-pip
sudo python3 -m pip install --upgrade --user pip setuptools
sudo python3 -m pip install --upgrade --user Cython==0.29.19 pillow
sudo python3 -m pip install --user https://github.com/kivy/kivy/archive/master.zip
sudo apt install python3-rpi.gpio
sudo python3 -m pip install paho-mqtt
My Python must run as root
because of the shutdown script. So I need to install kivy as root (with sudo). Change the dimensions on 480/320 in /root/.kivy/config.ini
.The screen driver and the touchscreen controller driver have separate settings for screen rotation. So we need to change the rotation of the touchscreen controller driver to match the rotation of the screen driver.
Add under [input]
in /root/.kivy/config.ini
the following line to get the touchscreen work correctly (again hours of research):
hid_%(name)s = probesysfs,provider=hidinput,param=rotation=270,param=invert_y=1
The 3.5" display is from adafruit (PiTFT). We download there installer script and use it:
wget https://raw.githubusercontent.com/adafruit/Raspberry-Pi-Installer-Scripts/master/adafruit-pitft.sh
chmod +x adafruit-pitft.sh
sudo ./adafruit-pitft.sh
I selected the configuration number 5 (PiTFT 3.5" resistive touch (320x480)) and the rotation number 3 (270 degrees (landscape)). As we want to use
PiTFT as text console, theoretically we have to say "Yes" to the question "Would you like the console to appear on the PiTFT display." But with this I got the console on the screen, but kivy didn't use the screen.
So say no
for "Would you like the console to appear on the PiTFT display" and yes
for "Would you like the HDMI display to mirror to the PiTFT display?". Now the fbcp files were installed. After this I ran the script again and got back to the first option with a functioning display!
The information on he rotation is stored in /boot/config.txt
, so it is possible to change it there if needed.
Look for more infos here.
After installation you can test with a minimal Python program:
from kivy.app import App
class pitouconApp(App):
pass
if __name__ == '__main__':
pitouconApp().run()
With the following pitoucon.kv
program (same folder):
Screen:
Label:
text: 'Hallo'
Now kivy is working and also the touch display. But when I started the script on boot with rc.local
the kivy screen breaks when something is shown on the console. First I tried to redirect the output to /dev/null
, but that was not enough. The solution was to get the console screen out of the way.
Change in /etc/cmdline.txt
fbcon=map:10
to fbcon=map:2
.
To start the script, add the following line to your /etc/rc.local
file (sleep waits for the network):
(sleep 10
python3 /home/pi/pitoucon/pitoucon.py) &
I added a push-button (Pin 40 (GPIO21) to Ground) to my pitoucon. When the button is pressed for less than 3 seconds, my Pi reboots. If pressed for more than 3 seconds it shuts down.
The cool script is on github.com/gilyes/pi-shutdown. Download the script and add the following line to /etc/rc.local
:
python3 /home/pi/pishutdown.py &
(If you use pin 5 (GPIO3) and it is pressed while shut down, the Pi restarts. This is not possible with pin 40.)
My Pi became randomly inaccessible over WiFi. In /var/log/syslog
I found:
{TIMESTAMP} raspberrypi dhcpcd[{PID}]: wlan0: carrier lost
It turned out the problem was that the Pi WiFi controller has power_save on by default.
Command to read the current power saving mode (Stretch):
sudo iw wlan0 get power_save
Command to power_save off:
sudo iw wlan0 set power_save off
To make this permanent I added the following line to /etc/rc.local
:
/sbin/iw dev wlan0 set power_save off
My /etc/rc.local
after all the changes:
_IP=$(hostname -I) || true
if [ "$_IP" ]; then
printf "My IP address is %s\n" "$_IP"
fi
/usr/local/bin/fbcp &
/sbin/iw dev wlan0 set power_save off
(sleep 10
python3 /home/pi/pitoucon/pitoucon.py) &
python3 /home/pi/pishutdown.py &
exit 0
To understand the Kivy logic was not so easy, but finally the Python software worked. A switch on GPIO 3 is used to toggle the backlight. The switch is polled every half of a second in a callback function that is initiated by an event (see https://kivy.org/docs/guide/events.html). Another event every 10 minutes sends an alive message.
I wanted to use the Screenmanager with more screens, but finally one screen sufficed. The pitoucon.kv
-file is in the download section. Here is the Python code (the MQTT messages changed from the previous version!):
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from kivy.app import App
from kivy.clock import Clock
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import ObjectProperty, StringProperty
import paho.mqtt.client as mqtt
import RPi.GPIO as GPIO
import json
import time, datetime
topic = "myhome/ventilation"
class ScreenManagement(ScreenManager):
pass
class Main(Screen):
inoutflow = StringProperty()
freshexh = StringProperty()
tacho = StringProperty()
co2 = StringProperty()
def setVent(self,percent):
print('button state is: ', percent)
message = "{\"flow-rate\":" + str(percent) + "}"
mqttc.publish(topic,message)
class Setup(Screen):
pass
class pitouconApp(App):
def build(self):
global SM #ScreenManager
global s
SM = self.root
s = SM.get_screen('main')
def on_start(self):
global mqttc
global switch_pin
switch_pin = 3
global switch_old
switch_old = 1
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
GPIO.setup(switch_pin, GPIO.IN)
clientID = "pitoucon"
brokerIP = "192.168.1.111"
brokerPort = 1883
# Callback if CONNACK response from the server.
def onConnect(client, userdata, flags, rc):
print("Connected with result code " + str(rc))
mqttc.subscribe(topic, 0) # Subscribe (topic name, QoS)
# Callback that is executed when we disconnect from the broker.
def onDisconnect(client, userdata, message):
print("Disconnected from the broker.")
# Callback that is executed when subscribing to a topic
def onSubscribe(client, userdata, mid, granted_qos):
print('Subscribed on topic.')
# Callback that is executed when unsubscribing to a topic
def onUnsubscribe(client, userdata, mid, granted_qos):
print('Unsubscribed on topic.')
# Callback that is executed when a message is received.
def onMessage(client, userdata, message):
io=message.payload.decode("utf-8")
if (io[2:6] != "flow") and (io[2:7] != "alive"):
try:
ioj=json.loads(io)
inflow_tmp = ioj['air_flows']['in']['temp_C']
inflow_hum = ioj['air_flows']['in']['hum_%']
outflow_tmp = ioj['air_flows']['out']['temp_C']
outflow_hum = ioj['air_flows']['out']['hum_%']
in_out_txt = "Temp Hum\n" + str(inflow_tmp) + "°C " + \
str(inflow_hum) + "% IN\n\n" + " " + str(outflow_tmp) + \
"°C " + str(outflow_hum) + "% OUT"
s.inoutflow = in_out_txt
fresh_tmp = ioj['air_flows']['fresh']['temp_C']
fresh_hum = ioj['air_flows']['fresh']['hum_%']
exhaust_tmp = ioj['air_flows']['exhaust']['temp_C']
exhaust_hum = ioj['air_flows']['exhaust']['hum_%']
print(fresh_tmp)
print(fresh_hum)
print(exhaust_tmp)
print(exhaust_hum)
fresh_exh_txt = " Temp Hum\nFRESH " + str(fresh_tmp) + \
"°C " + str(fresh_hum) + "%\n\nEXHAUST " + \
str(exhaust_tmp) + "°C " + str(exhaust_hum) + "%"
s.freshexh = fresh_exh_txt
print(fresh_exh_txt)
tacho_in = ioj["tacho"]["in_%"]
tacho_out = ioj["tacho"]["out_%"]
tacho_txt = "Tacho I/O : " + str(tacho_in) + "% " + str(tacho_out) + "%"
s.tacho = tacho_txt
print(tacho_txt)
co2 = ioj["co2_ppm"]
co2_txt = "CO2: " + str(co2) + "ppm"
s.co2 = co2_txt
print(co2_txt)
except:
print("json error")
else:
pass
# Callback every 500ms to poll the backlight switch and act accordingly
def poll_switch(dt):
global switch_old
switch = GPIO.input(switch_pin)
if (switch != switch_old):
if switch:
with open("/sys/class/backlight/soc:backlight/brightness", "w") as f:
f.write('0')
else:
with open("/sys/class/backlight/soc:backlight/brightness", "w") as f:
f.write('1')
switch_old = switch
def send_alive(dt):
samessage = datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S')
samessage = "{\"alive\":\"" + samessage + "\"}"
mqttc.publish(topic,samessage)
mqttc = mqtt.Client(client_id=clientID, clean_session=True)
mqttc.on_connect = onConnect # define the callback functions
mqttc.on_disconnect = onDisconnect
mqttc.on_subscribe = onSubscribe
mqttc.on_unsubscribe = onUnsubscribe
mqttc.on_message = onMessage
mqttc.connect(brokerIP, brokerPort, keepalive=60, bind_address="")
mqttc.loop_start() # start loop to process callbacks! (new thread!)
event = Clock.schedule_interval(poll_switch, 1 / 2.) # poll switch 500ms
event = Clock.schedule_interval(send_alive, 600) # send alive 10 min.
if __name__ == "__main__":
pitouconApp().run()
For the housing I searched for PiTFT on thingiverse and found the OctoPrint housing. But there exist 2 versions of the 3.5" PiTFT with different dimensions. I got Version with the PID 2097 and not the newer PID 2441. So a second search gave me the Touch Pi housing from adafruit. I changed the bottom stl, so a raspi3 could be mounted and added blocks to fix it on the wall. Fortunately there is enough space and the possibility to add a switch for the backlight was given.
I also added a push-button to reset or shutdown the Raspi (not shown in this picture).