RetroPie forum home
    • Recent
    • Tags
    • Popular
    • Home
    • Docs
    • Register
    • Login
    Please do not post a support request without first reading and following the advice in https://retropie.org.uk/forum/topic/3/read-this-first

    Graceful GPIO reboot/shutdown (saves srm and metadata) Python functions

    Scheduled Pinned Locked Moved Help and Support
    emulationstaionmetadata issuesshutdown scriptretroarchgpio
    6 Posts 4 Posters 1.1k Views
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • NachosN
      Nachos
      last edited by Nachos

      I've been lurking the forums for a while and wanted to contribute some Python code I put together in order to accomplish GPIO button integration for game reset (or ES restart), graceful reboot, and graceful shutdown. This allows RetroArch to save .srm data and allows ES to save metadata properly. I'm leaving out the GPIO-specific code since that will be different for everyone, but I'm successfully using this on an rpi3 with the Kintaro Kuma case, which has a soft power and reset button.

      Note: This uses RetroArch network commands, so it would need modification to work with non-libretro cores.

      Kudos to @meleu for his killes.sh script, which I based this on when starting out, and to all those who have contributed to it and other shutdown scripts. Thanks to @cyperghost for his very helpful feedback and contributions as well. Most of the ideas here are not my own, I just collected them together from various forum posts and code snippets into a single Python3 script:

      #!/usr/bin/python3 -u
      
      import errno
      import os
      import re
      import shutil
      import signal
      import socket
      import subprocess
      import time
      from pathlib import Path
      
      class vars():
          pid_timeout = 10        # Timeout PID wait after 10sec
          sleep_time = 0.2        # Sleep for 1/5sec while polling
      
      def touch_file(pathstr):
          Path(pathstr).touch()
          shutil.chown(pathstr, "pi", "pi")
      
      def get_pid(ex):
          pid = 0
          try:
              pid_str = subprocess.check_output(['pgrep', '-f', '-o', ex]).strip().decode()
              pid = int(pid_str)
          finally:
              return pid
      
      def get_run_pid():
          ex = '/opt/retropie/supplementary/runcommand/runcommand.sh'
          return get_pid(ex)
      
      def get_es_pid():
          ex = '/opt/retropie/supplementary/.*/emulationstation([^.]|$)'
          return get_pid(ex)
      
      def wait_pid(pid):
          if pid < 1:
              return False                # Invalid PID
          wait_counter = 0
          hold_time = vars.pid_timeout / vars.sleep_time
          while True:
              wait_counter += 1
              if wait_counter > hold_time:
                  return False            # Timeout
              try:
                  os.kill(pid, 0)
              except OSError as err:
                  if err.errno == errno.ESRCH:
                      return True         # No such PID
              time.sleep(vars.sleep_time)
      
      def kill_es():
          run_pid = get_run_pid()
          if run_pid:
              net_command("QUIT")
              wait_pid(run_pid)
          es_pid = get_es_pid()
          if es_pid:
              os.kill(es_pid, signal.SIGTERM)
              wait_pid(es_pid)
      
      def net_command(command):           # RetroArch network command
          try:
              s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
              s.connect(("127.0.0.1", 55355))
          except socket.error as err:
              print("Socket error: ", err)
          else:
              s.send(command.encode())
          finally:
              if s: s.close
      
      def restart_es():
          es_pid = get_es_pid()
          if es_pid:
              touch_file("/tmp/es-restart")
              os.kill(es_pid, signal.SIGTERM)
      
      def do_reset():
          if get_run_pid():
              net_command("RESET")
          else:
              restart_es()
      
      def do_reboot():
          kill_es()
          os.system("sudo reboot")
      
      def do_shutdown():
          kill_es()
          os.system("sudo shutdown -h now")
      

      RetroArch network commands need to be enabled, by setting in /opt/retropie/configs/all/retroarch.cfg

      network_cmd_enable = true
      

      Obviously these are all just utility functions, you'll need to wire them up to your GPIO code. Personally, I call do_shutdown() for the power switch, do_reset() for a momentary push and release of the reset switch, and do_reboot() after holding down the reset switch.

      Some findings while writing and testing this:

      By default, RetroArch only commits .srm data to disk when it gracefully exits. Using kill on the process started by runcommand seems to prevent this from happening. Sending a "QUIT" via network command allows it to exit gracefully (equivalent to the quit hotkey).

      There is a necessary delay between quitting the emulator core and sending a kill signal to ES, otherwise "Terminated." would be printed to the console and metadata would not be saved. I'm guessing that ES needs a bit of time to return control to itself. It might be possible to use runcommand-onend.sh to get the timing for this more precise, but I haven't tried it. In testing, 1.5 seconds delay after wait_pid(pid) was always enough, so I set it to 2 seconds in the example code. The time might need tweaking on other systems.

      Edit: As @cyperghost pointed out, simply waiting for the runcommand.sh PID eliminates the need for a sleep. The code above has been edited to reflect this. Here is the removed function to find the emulator core PID, in case anyone needs it:

      import linecache
      
      def get_core_pid():
          run_line = linecache.getline('/dev/shm/runcommand.info', 4)
          if not run_line:
              return 0
          ex = re.escape(run_line.strip().split(' ', 1)[0])
          return get_pid(ex)
      

      When sending a kill to ES, it is important to wait for its PID to finish up before issuing a shutdown or reboot command in order for metadata to be saved. The wait function provided by the OS will not work if the PID you are waiting for is not a child process of the waiting process, so I use the wait_pid(pid) function instead to do a check in a loop (with a sleep interval and timeout) via os.kill(pid, 0) which does not send a kill signal but rather throws a errno.ESRCH exception when the PID does not exist.

      If an emulator is not running when do_reset() is called, rather than sending "RESET" via network command to RetroArch, ES is restarted instead. You could change this if you don't want ES restart functionality. In order to accomplish the restart, /tmp/es-restart is touched and a kill signal sent to ES. The emulationstation.sh script will see the temporary file, remove it, and do the restart for us (equivalent to selecting "Restart EmulationStation" from the ES menu).

      The killes.sh script and shutdown service by @meleu that I referenced while making this, there is a lot of good information in here:

      ensuring ES gracefully finish and save metadata in every system shutdown

      It is still probably the best, most general approach to accomplish clean shutdowns. I'm sharing my code primarily for others who are comfortable editing Python code and already have Python-based GPIO code that they want to integrate clean reboot/shutdowns into directly. @cyperghost wrote a bash script with similar functionality for anyone who would prefer using bash, and there is good info here as well:

      posting: Yet annother NESPi case

      I hope this is helpful for anyone looking to roll a custom GPIO script for soft buttons!

      cyperghostC 1 Reply Last reply Reply Quote 3
      • cyperghostC
        cyperghost @Nachos
        last edited by

        @nachos said in Graceful GPIO reboot/shutdown (saves srm and metadata) Python functions:

        It might be possible to use runcommand-onend.sh to get the timing for this more precise, but I haven't tried it.

        I think a better way is to process PID from runcommand.sh itself. As long as it's running you are not in the ES menu. If this PID isn't available anymore then you can be sure you are back in ES. Then send kill PID to ES.

        NachosN 1 Reply Last reply Reply Quote 0
        • NachosN
          Nachos @cyperghost
          last edited by

          @cyperghost Great suggestion, thanks. It would certainly be cleaner. I'll edit the post once I can change the code and test.

          cyperghostC 1 Reply Last reply Reply Quote 0
          • cyperghostC
            cyperghost @Nachos
            last edited by cyperghost

            @nachos No problem ... I used a similar method like your python script in this posting: Yet annother NESPi case.

            There are funtions to determinine PIDs and then use a do-loop as long as PID is active. Very similar to yours ....
            But I use the PID from the runcommand also and therefore no sleep command is needed.

            # Power button pressed?
            if [ $power = 1 ]; then
                es_action check
                [[ -n $emupid ]] && kill $emupid && es_wait $emupid && es_wait $rcpid
            

            Function es_action check gives feedback of all running processes
            Then I check if an emulator is active, if yes ... kill it and wait as long as pid of runcommand is active.

            EDIT:
            Closing words: The shutdown service is imho the best solution. Because you simple do not have to care for the shutdown command itself. Use any coding language you want and initiate a shutdown. And the metadata will always be saved. Believe me without the work @meleu @TMNTturtlguy @cyperghost and much much other helpers and testers this won't be possible ;)

            1 Reply Last reply Reply Quote 0
            • P
              psb
              last edited by psb

              @Nachos I know this is an old thread, but this is just the thing I have been looking (for a very long time) for!

              Please excuse the noob question, but how would I call one of these functions from the command line? Can I use, for example, the quit retroarch function as a bash command or is there another way to call the function?

              Any help would be much appreiated!

              mituM 1 Reply Last reply Reply Quote 0
              • mituM
                mitu Global Moderator @psb
                last edited by

                @psb The poster hasn't been on this forum for 1 year and bumping old topics is bad forum etiquette. If you have a question, then open a new topic and ask away - don't re-open years old topics.

                1 Reply Last reply Reply Quote 0
                • First post
                  Last post

                Contributions to the project are always appreciated, so if you would like to support us with a donation you can do so here.

                Hosting provided by Mythic-Beasts. See the Hosting Information page for more information.