The Problem
I have a raspberry pi 3b+ running a python 3.7 script that is controlling an arduino. The arduino is reading values in from a few different sensors, and sending the data back to the python code, where it is processed and stored. These sensors are measuring temperature, humidity, ambient light level, soil moisture, and water levels of an agroponic system, with the purpose of maintaining ideal values to facilitate plant growth. This is a timer based system, and will turn the grow light on after 16 hours to run for 8 hours and repeat, and the pump will turn on after 18 hours and run for 5 seconds (it's a strong pump) and repeat.
Obviously, plants don't grow in the span of a few hours, and this script would ideally be running for a few months at a time. As the title suggests, that's not happening. To make make matters more confusing, I have a similar system that works by responding to the values read from the sensors, and turns on the grow light or pump as needed when indicated by the sensors. The only difference in the code is instead of a timer, it responds to the sensor input. It works, and works well. In my last test, it ran for a month and a half, and only stopped because I was using a submersible water pump that ended up corroding (I have a peristaltic pump now).
Ideally, both of these systems are started by opening up a terminal on the pi, and running python3 arduinoDriver.py
Things I've tried
- Restarted the raspberry pi running the timer system
- Updated os via prompt in the top right of the desktop that says "updates are available" or something like that
- Set Try/Except throughout the code, including a global try/catch arounds the arduinoDriver file itself, where in the except statements, used the logging module to write output to a file on the machine. We ran the script, and the next day, upon seeing that it had stopped, checked the log file, only to see that nothing had been logged
- Took the sd card out of the raspberry pi, and put it into a different raspberry pi 3b+
- Re-parented the script by running the command
setsid python3 arduinoDriver.py < /dev/zero &> /dev/null &
as mentioned in this post - Running the script through tmux
System Specs
Raspberry pi model 3b+ Raspios Buster 05/07/2021 is the name the iso file has from the download. I can't find it on their website, and as mentioned above, I updated it as a last ditch attempt to figure out what was wrong before coming here. When I get back to the school tomorrow, I can get the specific release if that would be helpful Arduino Mega 2560
Things I know
- When the script runs, the print statements showing the input from the sensors work, so I know the mega and the sensors are working
- When I get to the system after the process crashes, the terminal window that had it open, as well as any other windows that were opened, are closed. I opened up the browser on the pi itself to start troubleshooting, and I was connected to the internet. This is important because every time the raspberry pi restarts, we have to connect it to the school's guest wifi. This means the system didn't turn off and back on
- From the database table storing our sensor data, we can tell how long (approximately) the script was running. It's lasted anywhere from 15 minutes to 4.5 hours
- The empty log file screams that this is a hardware issue. The fact that this is an issue on 2 separate raspberry pis suggests that this is a software issue
- The only issues I'm aware of that sort of fit the symptoms are memory related. Maybe something incremented to a value too large for the data type to hold, causing a memory issue. Maybe the 1gb of ram on the pi is not enough. From the attached code though, I don't think anything is actually ever incrementing, but rather being overwritten. It still doesn't make sense though when you consider the fact that our sensor-responsive system works perfectly.
The 2 systems were meant to run against each other as a comparison for my (and my 2 classmates') senior project. Given the circumstances, our professors (who are also stumped by this problem) have allowed us to axe the timer system from the research, since the plants need time to grow before finals in May. We're now simply comparing the sensor-reactive system to the efforts of humans watering and taking care of a plant by hand. One of the professors suggested that I reach out to this board and ask for help. I'll be checking in on this relatively frequently, so please let me know if I can supply any more information! I realize the code isn't the most beautiful thing, but like I said, the sensor-reactive code is very similar, so we didn't want to change how it functioned so that way we could keep the research comparable
The script
# Third Party
from datetime import datetime, timedelta
import serial
import time
import logging
# Proprietary
from controllers.sendEmail import notifyLowWater, notifyWaterFilled
from controllers.sendData import checkIfDataNeedsSent
from controllers.signalArduino import determineSignalToSend
from controllers.waterPump import checkIfPumpNeeded
from controllers.lightValue import checkIfLightNeeded
from controllers.dataArray import DataArray
from controllers.database import Database
LOG_FORMAT = "%(levelname)s %(asctime)s - %(message)s"
logging.basicConfig(filename="error_log.log",
filemode="w",
format=LOG_FORMAT,
level=logging.ERROR)
#Sets up the mega 2560
board = serial.Serial(
port = '/dev/ttyACM0',
baudrate = 115200,
timeout = None,
)
#Default starting values, needed just to have these guys be global
temp = 70
hum = 20
moisture = 0
timeToKeepLightOn = timedelta(hours=8)
timeToKeepLightOff = timedelta(hours=16)
# timeToKeepLightOn = timedelta(minutes=5) These 2 lines were for testing the script in the classroom
# timeToKeepLightOff = timedelta(seconds=20)They Worked
lightStartOn = datetime.now() - timeToKeepLightOn #When the light turned on
timeLightOn = 0 #This is how long the light has actually been on
isLightOn = False
lightBool = True #This is for the while board.inWaiting() to make the function only execute once
lightOn = False
# lightArray = DataArray(101, 20)
#These are for the same purposes as the light variables above, but for our pump
timeToKeepPumpOn = timedelta(seconds=5)
timeToKeepPumpOff = timedelta(hours=18)
pumpStartTime = datetime.now() - timeToKeepPumpOn
timePumpOn = 0
isPumpOn = False
pumpBool = True
floatFlag = 'LOW' #This is so we know if we have water in our resevoir from our float sensor
emailSent = False
emailTimestamp = 0
moistureHigh = 450 #Not important. We copied our sensor-reactive code into a new file and then added the time delta stuff above to make this for a timer
moistureArray = DataArray(moistureHigh, 5)
timeDataCollected = 0 #Just a time stamp for when we store it. Honestly redundant
lastMinuteSent = 1 #We store our data on the 15, 30, 45, and 0 mark of every hour. This ensures we don't collect duplicates
envId = 1 #We built our schema to be expandable. This is unimportant, just need it to be able to store our data
signalSentBool = False #Checks if we have sent a signal to the mega or not, so we only do it once
db = Database()
def checkIfEmailNeeded(floatFlag, emailTimestamp): #Commented out, not important
global emailSent
currentTime = time.time()
if(currentTime - emailTimestamp > 86400):#86400 seconds in 24 hours
emailSent = False
if(floatFlag == 'LOW' and not emailSent):
notifyLowWater(currentTime)
emailSent = True
emailTimestamp = time.time()
if(floatFlag == 'HIGH' and emailSent):
notifyWaterFilled(currentTime)
emailSent = False
return emailTimestamp
while True:
try:
while(board.inWaiting() == 0):
#emailTimestamp = checkIfEmailNeeded(floatFlag, emailTimestamp)
if pumpBool:
pumpStartTime, isPumpOn, endTime = checkIfPumpNeeded(floatFlag, pumpStartTime, isPumpOn, timeToKeepPumpOn, timeToKeepPumpOff) #Returns the time the pump started, if it's currently on or not, and if the pump stopped
if endTime:
timePumpOn += int((datetime.now() - pumpStartTime).total_seconds()) #If it stopped, compute how long it was on
pumpBool = False
returned = checkIfDataNeedsSent(lastMinuteSent, temp, hum, moistureArray.getAvg(), lightStartOn, timePumpOn, timeDataCollected, envId, db)
if returned != lastMinuteSent: #Returned is a 0, 15, 30, or 45, just to make sure we don't send data a bunch of times
lastMinuteSent = returned
timeLightOn = 0
timePumpOn = 0
if lightBool:
lightStartOn, isLightOn, endTime = checkIfLightNeeded(lightStartOn, isLightOn, timeToKeepLightOn, timeToKeepLightOff) #Same logic as the pump
if endTime:
timeLightOn += int((datetime.now() - lightStartOn).total_seconds()/60)
lightBool = False
if not signalSentBool:
determineSignalToSend(isPumpOn, isLightOn, board)
signalSentBool = True
timeDataCollected = datetime.now().strftime('%Y-%m-%d-%H:%M:%S')
output = board.readline().decode('utf-8', 'ignore').strip().split(',')
if len(output) == 6: #reads data from sensors
# temp = output[0]
# hum = output[1]
moisture = int (output[2])
moistureArray.add(moisture)
# lightArray.add(output[3])
if 'LOW' in output[4]:
floatFlag = 'LOW'
else:
floatFlag = 'HIGH'
pumpBool = True
lightBool = True
signalSentBool = False
logging.info(output)
else:
logging.info("Incomplete board output: ", output)
continue
except Exception as error:
logging.error('**Error reading board: ', error)
cron
to schedule tasks, and run independent tasks independently.systemd start mything
. Of course, you have to create amything
service first. If you can write a python3 program, you can write a systemd service file.cron
is simple to use.