doobsky
doobsky

Reputation: 21

Simple python script using 100% CPU on rPi3B+

I have raspberry pi model 3b+ with a HC-SR04 ultrasonic distance sensor (there is also a couple of ds18b20 and a DHT21 but I think they're unrelated to my problem).

I have found a python script to make measurements from it and modified it to my needs - mostly to take a couple of reading spanned in time, take an average from it and map the value to range from 0 to 100, as the percentage and commit it to the influx database for grafana and domoticz.

The code:

#source: https://tutorials-raspberrypi.com/raspberry-pi-ultrasonic-sensor-hc-sr04/

#Libraries
import RPi.GPIO as GPIO
import time
from influxdb import InfluxDBClient

import sys
# https://www.domoticz.com/wiki/Domoticz_API/JSON_URL%27s#Python
import requests
 
client = InfluxDBClient(database='pellet')
series = [] 
 
#GPIO Mode (BOARD / BCM)
GPIO.setmode(GPIO.BCM)
 
#set GPIO Pins
GPIO_TRIGGER = 23
GPIO_ECHO = 22
 
#set GPIO direction (IN / OUT)
GPIO.setup(GPIO_TRIGGER, GPIO.OUT)
GPIO.setup(GPIO_ECHO, GPIO.IN)
 
def distance():
    # set Trigger to HIGH
    GPIO.output(GPIO_TRIGGER, True)
 
    # set Trigger after 0.01ms to LOW
    time.sleep(0.00001)
    GPIO.output(GPIO_TRIGGER, False)
 
    StartTime = time.time()
    StopTime = time.time()
 
    # save StartTime
    while GPIO.input(GPIO_ECHO) == 0:
        StartTime = time.time()
 
    # save time of arrival
    while GPIO.input(GPIO_ECHO) == 1:
        StopTime = time.time()
 
    # time difference between start and arrival
    TimeElapsed = StopTime - StartTime
    # multiply with the sonic speed (34300 cm/s)
    # and divide by 2, because there and back
    distance = (TimeElapsed * 34300) / 2
 
    return distance

def pellet(dist):
   # zmierzona odleglosc
   # dist = distance()
   # do zmierzenia poziom maksymalny
   # 63 - do pokrywy
   in_min = 63
   # do zmierzenia poziom minimalny
   in_max = in_min + 100
   
   #wyjscie jako procent, od 0 do 100
   out_min = 100
   out_max = 0

   # map z arduino: https://www.arduino.cc/reference/en/language/functions/math/map/
   return (dist - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;

def loop():
   # nie wiecej jak 200 iteracji
   loop = 200
   # suma
   total = 0
   # tabelka z pojedynczmi wynikami
   measurements = []
   # liczba pomiarow do zrobienia
   counter = 10
   counter1 = 0
   # czas pomiedzy pomiarami
   sleep =30
   # 
   while loop > 0:
      loop -= 1
      time.sleep(sleep)
      # koniec, jesli wykonano liczbe pomiarow
      if counter == 0:
         #print(total/10)
         return pellet(total/10), measurements
         break
      if loop == 0 and counter1 != 0:
         return pellet(total/counter1), measurements
         break
      if loop == 0 and (counter1 == 0 or total == 0):
         GPIO.cleanup()
         sys.exit()
      dist = distance()
      # jesli wynik jest zly
      if dist < 63 or dist > 163:
         print("nie ok")
         continue
      counter -= 1
      measurements.append(dist)
      counter1 += 1
      total += dist
      print("total po ",counter1 , "sek: ", total, "dist: ", dist)
   print(total/10)
   #return total/10

 
if __name__ == '__main__':
    try:
        #dist = distance()
        #print ("Measured Distance = %.1f cm" % dist)
        #print (pellet(dist))
        loop=loop()
        print("avg :", loop[0])
        #print("measurs :", loop[1])
        #print("test :", loop[1][2])
        if (1):
           point = {
              "measurement": "pellet",
              "tags": {
                 "location": "piwnica",
                 "type": "hc-sr04"
              },
              "fields": {
                 "value": loop[0],
                 "raw_measurement1": loop[1][0],
                 "raw_measurement2": loop[1][1],
                 "raw_measurement3": loop[1][2],
                 "raw_measurement4": loop[1][3],
                 "raw_measurement5": loop[1][4],
                 "raw_measurement6": loop[1][5],
                 "raw_measurement7": loop[1][6],
                 "raw_measurement8": loop[1][7],
                 "raw_measurement9": loop[1][8],
                 "raw_measurement10": loop[1][9]
              }
           }
           series.append(point)
           client.write_points(series)

        url = 'http://localhost:8080/json.htm?type=command&param=udevice&nvalue=0&idx=13&svalue='+str(loop[0])
        r = requests.get(url)
        
        GPIO.cleanup()
        # Reset by pressing CTRL + C
    except KeyboardInterrupt:
        print("Measurement stopped by User")
        GPIO.cleanup()

The problem is I noticed that the CPU temperature graph was elevated, with many short valleys to the about correct temperature.

enter image description here

When I ssh'd to the pi and run htop I saw that it was this script that is using 100% cpu.

enter image description here

But the weirdest thing is that the script is running in crontab every 15 minutes since yesterday, from about 14:30 and raise CPU temp started today at around 11:00.

enter image description here

enter image description here

I'm not a developer or a programmer and I just mostly copied the code from around the web so I don't know if this is some part of the code that did this (but why after 21 hours?) or what and why, and how to debug and fix it.

enter image description here

so it isn't just enviromental thing as the pi is in the attic where is about 5C to 10C.

Thank you for your help.

Upvotes: 1

Views: 1672

Answers (2)

Lev M.
Lev M.

Reputation: 6269

While it is impossible to know for sure where the issue lies without debugging directly on your system, and a glance at the code reveals several possible bugs in the logic, the one place that is most likely to cause the issue is the distance function.

As @mdurant already pointed out, your read loops will jump the CPU usage to 100%, but I suspect there is also another issue:
The trigger code and the read code are time sensitive!

The problem is, we don't know how much time actually passes between

# set Trigger to HIGH
GPIO.output(GPIO_TRIGGER, True)

# set Trigger after 0.01ms to LOW
time.sleep(0.00001)
GPIO.output(GPIO_TRIGGER, False)

and:

# save StartTime
while GPIO.input(GPIO_ECHO) == 0:
    StartTime = time.time()

# save time of arrival
while GPIO.input(GPIO_ECHO) == 1:
    StopTime = time.time()

While this simple algorithm - pulse trigger, count return interval will work on a microcontroller like Arduino, it is not reliable on a full blown computer like Raspberry Pi.

Microcontrollers run a single thread, with no OS or task scheduling, so they run code in real time or as close to it as possible (borrowing a few interrupts here and there).

But in your case you are running an interpreted language on a multitasking operating system, without explicitly giving it any high priority.

This means, your process could be suspended just enough time to miss the return "ping" and get stuck in the first loop.

This may only happen rarely when something else puts a load on the Pi, which would explain why you only noticed the issue after 21 hours running.

You should implement some form of timeout for GPIO reading loops and return an error value from the distance function if that timeout is reached to ensure you do not have an infinite loop when something goes wrong with the hardware, or you miss the return ping due to scheduling issues.

I suggest something like this:

def distance():
    MAX_READ_ATTEMPTS = 10000 #this is a random value I chose. You will need to fine-tune it based on actual hardware performance



    # set Trigger to HIGH
    GPIO.output(GPIO_TRIGGER, True)

    # set Trigger after 0.01ms to LOW
    time.sleep(0.00001)
    GPIO.output(GPIO_TRIGGER, False)

    # lets not have any unneeded code here, just in case

    # save StartTime
    while GPIO.input(GPIO_ECHO) == 0 and retry_counter < MAX_READ_ATTEMPTS:
        retry_counter += 1
        StartTime = time.time()

    # maximum number of retries reached, returning with error condition
    if retry_counter == MAX_READ_ATTEMPTS:
        return -1
    
    reatry_counter = 0

    # save time of arrival
    while GPIO.input(GPIO_ECHO) == 1 and retry_counter < MAX_READ_ATTEMPTS:
        retry_counter += 1
        StopTime = time.time()

    # maximum number of retries reached, returning with error condition
    if retry_counter == MAX_READ_ATTEMPTS:
        return -1

    # time difference between start and arrival
    TimeElapsed = StopTime - StartTime
    # multiply with the sonic speed (34300 cm/s)
    # and divide by 2, because there and back
    distance = (TimeElapsed * 34300) / 2

    return distance

I am intentionally not adding a delay between reads, because I am not sure what the tolerance for this measurement is in terms of timing, and sleep functions can't guarantee an exact delay (again, due to OS / CPU scheduling).

A brief 100% CPU load should be worth it to ensure accurate and valid measurements, as long as it is not kept up for too long, which our retry counting method should prevent.

Note that my only experience with ultrasonic sensors is using Arduino where a special pulseIn function is used that takes care of the implementation details of measurement so this solution is mostly an educated guess and I have no way of testing it.

Upvotes: 0

mdurant
mdurant

Reputation: 28673

Here:

while GPIO.input(GPIO_ECHO) == 0:
    StartTime = time.time()

this says "if the pin is 0, save the time, if the pin is zero, save the time, if the pin...." incessantly. You'll want to wait a little time after each check

while GPIO.input(GPIO_ECHO) == 0:
    time.sleep(0.001)  # 1 ms
    StartTime = time.time()

The check itself probably takes ~us, so this will reduce CPU usage by 99%. You might want to do the same for the pin==1 case, depending on how accurate you need the times to be.

Upvotes: 4

Related Questions