GPIO state after a program abort or user exit

I hope this little topic is of some help

I’ve seen several user questions whereby when a program terminates incorrectly for whatever reason, it can leave the GPIO in a “busy” state. Usually requiring a reboot.

Instead of a reboot one can often find the owner process id and just kill it

e.g running some python code such as DummySpeech.py may show


File “/usr/lib/python3/dist-packages/lgpio.py”, line 458, in _u2i
raise error(error_text(v))
lgpio.error: ‘GPIO busy’

on subsequent runs.

To free this, find the owner process id…

ps -aux | grep python

shows something like …

root 1037 0.0 0.4 103336 19288 ? Ss 12:04 0:00 python /usr/sbin/wayvnc-control.py
pidog 3724 79.2 1.4 1644880 55612 pts/0 Sl 12:49 0:09 python DummySpeech.py
pidog 3727 0.0 0.0 6092 2024 pts/0 S+ 12:49 0:00 grep --color=auto python

Here PID 3724 is the owner pid associated with DummySpeech.py, so kill it with

kill -9 3724

It may be necessary to run this with sudo

Standalone robot boot and shutdown

Most Sunfounder software runs in a loop, until ctrl-c is pressed, cleanly freeing all resources.

This may not be what a user program requires, for example a joystick button or voice command may be required for a program to exit.

Often a user wants a program to be standalone, free of a controlling ssh terminal or network, for example to run at a friends house, or for kids to play with. This requires that the program starts on boot, usually via systemctl system services.

This works well. However, it also needs a clean way to shut down. Simply turning it off, will likely corrupt the SDCARD. For this scenario the user shutdown can use the python command

import os
os.system("sudo shutdown now")

as the response to whatever input, joystick voice etc is required.

Exiting a user program back to the shell.

One can use SIGINT (via ctrl-c) exactly as Sunfounder do. But if one wants to use an e.g button, voice command, joystick etc, then the GPIO will remain reserved.
This route is a a bit more convoluted. It requires the use of a parent wrapper to start and terminate the user program.

Here is a short dummy program, which will intentionally leave the GPIO busy, in order to demonstrate the use of the wrapper. (ignore the safe_shutdown for now, (call it mycode.py)

from pidog import Pidog
import os

def safe_shutdown(dog, exit_code=0):
    print("Stopping PiDog motion...")
    try:
        dog.body_stop()       # stop all servos
        dog.wait_legs_done()  # wait for actions to finish
    except Exception:
        pass

    print("Exiting Python process...")
    os._exit(exit_code)



if __name__ == "__main__":
      my_dog=Pidog()

#############################################################
      #Dummy code for demo purposes, replace wih own code
      count = 0
      VoiceCommand = "None"
      while (VoiceCommand != "Exit"):

        print("Iteration ",count,VoiceCommand)
        count += 1
        #Force Exit command on 5th iteration for demo only
        if (count == 5):
          VoiceCommand = "Exit"
      #End dummy lines to remove
#############################################################

      print("Closing pidog")
      safe_shutdown(my_dog, 0)

If you do run this example then GPIO can be recovered as shown in above post

But, if one wishes to exit the user program cleanly, with GPIO available for subsequent runs, such as above example, then use the following launch wrapper, (call it run_pidog.py)

import pexpect
import sys
import time

#
# Usage: python3 run_pidog.py mycode.py
#

if len(sys.argv) < 2:
    print("Usage: python3 wrap2.py mycode.py")
    sys.exit(1)

code_to_run = sys.argv[1]

# Launch your code inside a pseudo-terminal
child = pexpect.spawn(f"python3 {code_to_run}", encoding='utf-8')

# Print its output live
child.logfile_read = sys.stdout

# Wait for the user code to end naturally
child.expect(pexpect.EOF)

# Give it a moment .....
time.sleep(0.2)

# Now send a Ctrl-C/SIGINT through the PTY
child.sendcontrol('c')

# Wait for child to fully terminate
child.close()

print("\n[Wrapper] Script finished and cleanup done ")

run this as ..

python run_pidog.py mycode.py

Note, if you use other resources such as the camera, these will need to be added to the shutdown function.

This works because the wrapper owns the real lifecycle, it decides when the session is over, injects SIGINT when appropriate, then waits for child to exit and, If needed, escalates (SIGTERM, SIGKILL)

Hope this is of some value to someone!