I've hit a roadblock here and need some help...
I have a raspberry pi 4 with a DHT22 temp/humid sensor and flask to show the data on a simple webpage (data is also written to an sqlite3 database but that doesn't seem relevant to the problem). The sensor read is triggered every 15 mins from a cron job.
Everything has been working great (i have months of successful data gathering) until i recently tried to add an html button on the webpage to allow the user to force the sensor to read now instead of waiting for the next interval. It works the first time, but all subsequent attempts to read the sensor now fail with the following error (note my dht22 data wire is on gpio pin 4):
Unable to set line 4 to input
Based on my research so far (links below), it seems like a problem with a library used to interact with the gpio pins libgpiod_pulsein as when i find and kill this process from the terminal, everything works fine until the next time i try and trigger the sensor read from the button.
https://github.com/adafruit/Adafruit_CircuitPython_DHT/issues/27
RPI dht22 with flask: Unable to set line 4 to input - Timed out waiting for PulseIn message
I have already tried the two commonly cited fixes of (1) set Debug=False in flask, (2) try a different GPIO pin with no success or noticeable change in behavior. The only fix seems to be killing the problematic process but obviously this is not a long term solution. Some of these other pages seem to imply that libgpiod_pulsein is not exiting correctly but no solution was proposed for this and I'm not savvy enough to attempt a solution myself.
My questions are:
Am i approaching the 'read button' in a fundamentally wrong way? Is an html POST method a bad way to call a python script and if so what should i do instead? I'm trying to avoid adding new languages to this project (ajax etc.) just for 1 or 2 features but if that's the only way then I'm open to it...
Is there an error here that's creating the bad behavior of libgpiod_pulsein that can be fixed?
I've included some code below that seems relevant, the full code is available at https://github.com/nathansibon/raspberry_pi_plant_datalogger
Flask Server
from flask import Flask, render_template
from config import *
import csv
import collect_data
from forms import *
from time import sleep
app = Flask(__name__)
app.config['SECRET_KEY'] = 'sparklingcider' #TODO change to random number
@app.route('/', methods=['GET', 'POST'])
def index():
f_name = name.replace('_', '')
f_location = location.replace('_', '')
button = collect_data_button()
data = {
'name': f_name,
'location': f_location
}
# Get the current status variables from CSV
reader = csv.reader(open('webpage_sensor_data.csv'))
for row in reader:
if row[0] == '':
pass
else:
data[row[0]] = row[1]
# Get the daily of the variables from CSV
reader = csv.reader(open('webpage_daily_data.csv'))
for row in reader:
if row[0] == '':
pass
else:
data[row[0]] = row[1]
if button.is_submitted():
print('submit detected')
collect_data.do()
sleep(2)
return render_template('index.html', **data, button=button)
return render_template('index.html', **data, button=button)
# this method sets the chart images to expire after 5 mins so the browser will fetch new images instead of using the cache
@app.after_request
def add_header(response):
response.cache_control.max_age = 300
response.cache_control.public = True
return response
if __name__ == '__main__':
app.run(host='0.0.0.0')
Main HTML Page
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Current Data {{name}}</title>
<!-- this next part disables browser caching, otherwise the charts won't update! -->
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<style>
th, td {padding: 5px;}
</style>
</head>
<body>
<h1>Data from {{name}} located at {{location}}</h1>
<h1>Current:</h1>
<h2>Last read at {{time}}</h2>
<form action="" method="post"> {{ button.submit() }} </form>
<table>
<tr>
<th></th>
<th style='text-align:center'>Drybulb</th>
<th style='text-align:center'>Relative Humidity</th>
<th style='text-align:center'>RH for 1 kPa VPD</th>
<th style='text-align:center'>Vapor Pressure Deficit</th>
<th style='text-align:center'>Light</th>
</tr>
<tr>
<td>Indoor</td>
<td style='text-align:center'>{{indoor_drybulb}} °C</td>
<td style='text-align:center'>{{indoor_rh}}%</td>
<td style='text-align:center'>{{req_rh}}%</td>
<td style='text-align:center'>{{indoor_vpd}} kPa</td>
<td style='text-align:center'>{{indoor_lux}} lux</td>
</tr>
<tr>
<td>Outdoor</td>
<td style='text-align:center'>{{outdoor_drybulb}} °C</td>
<td style='text-align:center'>{{outdoor_rh}}%</td>
<td></td>
<td style='text-align:center'>{{outdoor_vpd}} kPa</td>
<td></td>
</tr>
</table>
<h1>Yesterday's Indoor Conditions:</h1>
<table>
<tr>
<th></th>
<th style='text-align:center'>Mean</th>
<th style='text-align:center'>High</th>
<th style='text-align:center'>High Time</th>
<th style='text-align:center'>Low</th>
<th style='text-align:center'>Low Time</th>
</tr>
<tr>
<td>Drybulb</td>
<td style='text-align:center'>{{yesterday_drybulb_mean}} °C</td>
<td style='text-align:center'>{{yesterday_drybulb_max}} °C</td>
<td style='text-align:center'>{{yesterday_drybulb_max_time}}</td>
<td style='text-align:center'>{{yesterday_drybulb_min}} °C</td>
<td style='text-align:center'>{{yesterday_drybulb_min_time}}</td>
</tr>
<tr>
<td>Relative Humidity</td>
<td style='text-align:center'>{{yesterday_rh_mean}}%</td>
<td style='text-align:center'>{{yesterday_rh_max}}%</td>
<td style='text-align:center'>{{yesterday_rh_max_time}}</td>
<td style='text-align:center'>{{yesterday_rh_min}}%</td>
<td style='text-align:center'>{{yesterday_rh_min_time}}</td>
</tr>
<tr>
<td>Vapor Pressure Deficit</td>
<td style='text-align:center'>{{yesterday_vpd_mean}} kPa</td>
<td style='text-align:center'>{{yesterday_vpd_max}} kPa</td>
<td style='text-align:center'>{{yesterday_vpd_max_time}}</td>
<td style='text-align:center'>{{yesterday_vpd_min}} kPa</td>
<td style='text-align:center'>{{yesterday_vpd_min_time}}</td>
</tr>
<tr>
<td>Light</td>
<td style='text-align:center'>{{yesterday_lux_mean}} lux</td>
<td style='text-align:center'>{{yesterday_lux_max}} lux</td>
<td style='text-align:center'>{{yesterday_lux_max_time}}</td>
<td></td>
<td></td>
</tr>
</table>
<h1>Last Week</h1>
<table>
<tr>
<img src="/static/last_week_drybulb.png" alt="drybulb graph" width="80%" height="auto">
</tr>
<tr>
<img src="/static/last_week_rh.png" alt="drybulb graph" width="80%" height="auto">
</tr>
<tr>
<img src="/static/last_week_vpd.png" alt="drybulb graph" width="80%" height="auto">
</tr>
<tr>
<img src="/static/last_week_lux.png" alt="lux graph" width="80%" height="auto">
</tr>
</table>
</body>
</html>
Flaskforms for button
from flask_wtf import FlaskForm
import wtforms as wt
from wtforms.fields import html5 as wt5
class collect_data_button(FlaskForm):
submit = wt.SubmitField('Read Now')
Collect Data Script (this is what cron runs every 15 mins)
from function_library import *
import logging, time, os, datetime
from config import *
# This is where the main code is kept for this script.
# We enclose it in a function so we can wrap it in a general purpose error-handler as shown below
# This makes writing log files much simpler and will work whether it's run from the shell or a cron job
def do():
# retrieve data from OpenWeatherMap API, then calculate additional metrics from retrieved data and append list
outdoor = get_outdoor_weather()
outdoor = calc_outdoor_weather(outdoor)
# retrieve data from sensor(s), then calculate additional metrics from retrieved data and append list.
indoor = get_indoor_all()
# Full path of database: /home/pi/share/env_datalogger/pi_X_data.db
update_db(name + '_data.db', indoor, outdoor)
update_web_vars_sensor(indoor, outdoor)
update_web_charts(name + '_data.db')
# Set all paths to the current directory so cron job will not crash, but code will still run if you move files later...
abspath = os.path.abspath(__file__)
dname = os.path.dirname(abspath)
os.chdir(dname)
logging.basicConfig(filename='logs/collect_data.log', level=logging.INFO)
logging.info('started @ ' + datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
# this 'if' is necessary to allow other scripts (such as buttons on the flask server) to trigger the system to read sensors, but prevent this from running on module import
if __name__ == "__main__":
# general purpose error handling to log file. helpful when executing from cron since you don't see errors
try:
do()
except Exception as e:
logging.exception('Error in main')
logging.info(e)
logging.info('completed @ ' + datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + '\n')
exit()
Imports for function_library (contains the functions below)
import math, numpy, sqlite3, time, logging, pyowm
import pandas as pd
import numpy as np
from config import *
from datetime import datetime
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
# These packages will raise an error if not running this code on a Pi
# Adding this error handling will allow coding and debugging on PC
try:
import board, adafruit_dht
except Exception as e:
logging.info('not connected to pi, problematic libraries not imported (1 of 2)')
logging.info(e)
pass
try:
import busio, adafruit_veml7700
except Exception as e:
logging.info('not connected to pi, problematic libraries not imported (2 of 2)')
logging.info(e)
pass
Indoor Sensor Read Function (inside function_library.py)
def get_indoor_all():
output = [
datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
pi_serial,
location
]
if sensors.get('temp_humid') != 'none':
# output is [0] datetime [1] serial [2] location [3] drybulb [4] rh
for i in get_indoor_weather():
output.append(i)
# output is now [0] datetime [1] serial [2] location [3] drybulb [4] rh [5] wet bulb [6] dew point, [7] vapor pressure deficit [8] rh for vpd [9] white_light [10] lux
output = calc_indoor_weather(output)
else:
logging.info('No temp/humidity sensor listed, adding placeholders')
for i in range(6):
output.append(0)
if sensors.get('light') != 'none':
# output is [0] datetime [1] serial [2] location [3] drybulb [4] rh [5] wet bulb [6] dew point, [7] vapor pressure deficit [8] rh for vpd [9] white_light [10] lux
for i in get_indoor_light():
output.append(i)
else:
logging.info('No light sensor listed, adding placeholders')
for i in range(2):
output.append(0)
# TODO add soil moisture sensor
return output
DHT22 Sensor Read (called by get_indoor_all)
def get_indoor_weather():
# Initial the dht device, with data pin connected to:
print('starting dht22')
dhtDevice = adafruit_dht.DHT22(board.D4)
# Unfortunately, this sensor doesn't always read correctly so error handling and noise reduction is included
max = 5
err_count = 0
drybulb = []
humid = []
output = []
while len(drybulb) < max and err_count < max :
logging.info('read: '+str(len(drybulb))+', err: '+str(err_count))
# The DHT22 is reportedly slow at reading, this wait hopefully prevents a bad read (i.e. wildly different than it should be) on fail
time.sleep(2)
try:
drybulb.append(dhtDevice.temperature)
humid.append(round(dhtDevice.humidity / 100, 2)) # convert RH from "50%" to "0.50" to match outdoor weather format and work in calculations
logging.info('success')
except Exception as ee:
#logging.info('Error reading Temp-Humid Sensor, retrying...')
logging.info(ee)
err_count += 1
output.append(std_filter(drybulb, 1, 2))
output.append(std_filter(humid, 1, 2))
return output