Python Forum
Pausing a loop with spacebar, resume again with spacebard
Thread Rating:
  • 2 Vote(s) - 3 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Pausing a loop with spacebar, resume again with spacebard
#1
I have a loop that opens and closes images.



from PIL import Image
import time
import subprocess
for i in bio:
   p = subprocess.Popen(["C:\Program Files\IrfanView\i_view64.exe",'C:\\Users\Moondra\\Bioteck_charts\{}.png'.format(i)])
   time.sleep(5)
   p.kill()
As you can see the loop iterates every 5 seconds. I would like to pause this loop with "spacebar" if I see an image I would like to study more in detail, and then resume the loop once I press space bar again. I would also like to escape out of the entire loop if I press ESC

I usually run my programs through IDLE or Jupyternotebook, but I could only find KeyPress readers (barring Tinker and GUIS which I haven't really studied yet) that work on the OS level. My OS is Windows 7.

Here is a sample of what I tried to do. I tried variations of this code using multiple while loops etc:
from PIL import Image
import time
import subprocess
import pickle

with open('Bioteck.pickle', 'rb') as file:
   bio = pickle.load(file)

from msvcrt import getch

key = ord(getch())
for i in bio:
       while key != 27:  #escape key
           p = subprocess.Popen(["C:\Program Files\IrfanView\i_view64.exe",'C:\\Users\Moondra\\Bioteck_charts\{}.png'.format(i)])
           time.sleep(5)
           p.kill()
           break

       if key == 32:  #Space bar)
           raw_input()
           if key == 32:
           #kill
               p.kill()
               continue


       if key == 27:
           os._exit()
However, pressing spacebar, just doesn't pause the loop. It just jumps to the next image in the loop.


Looking for suggestions of a better way to do this.

Thank you.
Reply
#2
when you run your Popen, it should be done like:
from the docs: https://docs.python.org/3.6/library/subprocess.html
Quote:Popen.communicate(input=None, timeout=None)

   Interact with process: Send data to stdin. Read data from stdout and stderr, until end-of-file is reached. Wait for process to terminate. The optional input argument should be data to be sent to the child process, or None, if no data should be sent to the child. If streams were opened in text mode, input must be a string. Otherwise, it must be bytes.

   communicate() returns a tuple (stdout_data, stderr_data). The data will be strings if streams were opened in text mode; otherwise, bytes.

   Note that if you want to send data to the process’s stdin, you need to create the Popen object with stdin=PIPE. Similarly, to get anything other than None in the result tuple, you need to give stdout=PIPE and/or stderr=PIPE too.

   If the process does not terminate after timeout seconds, a TimeoutExpired exception will be raised. Catching this exception and retrying communication will not lose any output.

   The child process is not killed if the timeout expires, so in order to cleanup properly a well-behaved application should kill the child process and finish communication:

    proc = subprocess.Popen(...)
    try:
        outs, errs = proc.communicate(timeout=15)
    except TimeoutExpired:
        proc.kill()
        outs, errs = proc.communicate()
   Note:
   The data read is buffered in memory, so do not use this method if the data size is large or unlimited.

   Changed in version 3.3: timeout was added.
Reply
#3
(May-25-2017, 09:50 PM)Larz60+ Wrote: when you run your Popen, it should be done like:
from the docs: https://docs.python.org/3.6/library/subprocess.html
Quote:Popen.communicate(input=None, timeout=None)

   Interact with process: Send data to stdin. Read data from stdout and stderr, until end-of-file is reached. Wait for process to terminate. The optional input argument should be data to be sent to the child process, or None, if no data should be sent to the child. If streams were opened in text mode, input must be a string. Otherwise, it must be bytes.

   communicate() returns a tuple (stdout_data, stderr_data). The data will be strings if streams were opened in text mode; otherwise, bytes.

   Note that if you want to send data to the process’s stdin, you need to create the Popen object with stdin=PIPE. Similarly, to get anything other than None in the result tuple, you need to give stdout=PIPE and/or stderr=PIPE too.

   If the process does not terminate after timeout seconds, a TimeoutExpired exception will be raised. Catching this exception and retrying communication will not lose any output.

   The child process is not killed if the timeout expires, so in order to cleanup properly a well-behaved application should kill the child process and finish communication:

    proc = subprocess.Popen(...)
    try:
        outs, errs = proc.communicate(timeout=15)
    except TimeoutExpired:
        proc.kill()
        outs, errs = proc.communicate()
   Note:
   The data read is buffered in memory, so do not use this method if the data size is large or unlimited.

   Changed in version 3.3: timeout was added.


I tried implementing the code the way the docs provided by I"m still not fully understanding it, nor exactly how I can apply p.communicate to do what I want to do.

from PIL import Image
import time
import subprocess
from msvcrt import getch
for i in bio:
   p = subprocess.Popen(["C:\Program Files\IrfanView\i_view64.exe", 'C:\\Users\Moondra\\Bioteck_charts\{}.png'.format(i)])
   key = ord(getch())
   try:
       outs, errs = p.communicate()
       if key == 32:
           ???
       else: 
           time.sleep(5)
           p.kill()
       print(outs)
   except Exception as e:
       p.kill()
       outs, errs = p.communicate() 
It opens up the image, but I have to manually close the image.
Once, I close the image(pressing x in the corner of the window), the next image isn't popping up and the program hangs in Jupyter Notebook.

I'm assuming outs is some sort of output data. It's not really printing  anything despite explicitly writing print.
Reply
#4
First, if you have any desire for this to work cross-platform, you shouldn't use msvcrt.  Or, at least, wrap it up so it'll work on other platforms, like so (stolen from myself):
import sys

def _find_getch():
   try:
       import termios
   except ImportError:
       # Non-POSIX. Return msvcrt's (Windows') getch.
       import msvcrt
       return msvcrt.getch

   # POSIX system. Create and return a getch that manipulates the tty.
   import tty
   def _getch():
       fd = sys.stdin.fileno()
       old_settings = termios.tcgetattr(fd)
       try:
           tty.setraw(fd)
           ch = sys.stdin.read(1)
       finally:
           termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
       return ch

   return _getch

getch = _find_getch()
Next, getch() is blocking, isn't it? So if you want images to pop in and out of existence without you needing to hit some key just to satisfy getch, then you'll need to use getch() in a separate thread, and use some sort of communication to get keypresses, so you don't block the image rotating. A queue would work fine for that.
Reply
#5
(May-26-2017, 08:23 PM)nilamo Wrote: First, if you have any desire for this to work cross-platform, you shouldn't use msvcrt.  Or, at least, wrap it up so it'll work on other platforms, like so (stolen from myself):
import sys

def _find_getch():
   try:
       import termios
   except ImportError:
       # Non-POSIX. Return msvcrt's (Windows') getch.
       import msvcrt
       return msvcrt.getch

   # POSIX system. Create and return a getch that manipulates the tty.
   import tty
   def _getch():
       fd = sys.stdin.fileno()
       old_settings = termios.tcgetattr(fd)
       try:
           tty.setraw(fd)
           ch = sys.stdin.read(1)
       finally:
           termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
       return ch

   return _getch

getch = _find_getch()
Next, getch() is blocking, isn't it?  So if you want images to pop in and out of existence without you needing to hit some key just to satisfy getch, then you'll need to use getch() in a separate thread, and use some sort of communication to get keypresses, so you don't block the image rotating.  A queue would work fine for that.

I haven't looked in to queue yet but I read a little about threading. I found a moudle called keyboard that works really well at capturing key presses and the documentation is really well done.

I've written some code below attempting to use threads, but the key presses are being read only in a very tiny time frame.  While the program is sleeping during the time.sleep(3) portion, no keys are being read.  Not sure why as I thought the separate thread would be a separate thread.



from threading import Thread


#function to use for threading
def keyboard_press():
   while p == True:
       if keyboard.is_pressed('down') == True:   # here I'm using 'down'  (down key) as the key to pause the cycle. 
           print('yes')
           input()   #I"m attempting to use this to pause the script/loop
           p.kill()
       #continue


with open('C:\\Users\Moondra\\Bioteck.pickle', 'rb') as file:
   bio = pickle.load(file)        #just getting a the name of images to open



for i in bio[:5]:

   p = subprocess.Popen(["C:\Program Files\IrfanView\i_view64.exe",'C:\\Users\Moondra\\Bioteck_charts\{}.png'.format(i)])
  
   t = Thread(target = keyboard_press, args =())
   t.start()   #I start the thread, which should run parallel with my program 
   time.sleep(3)   #normal continuation of the program, if the thread's `if` statement is not invoked. 
   p.kill()
What am I doing wrong here?
Reply
#6
You call p.kill() all the time, regardless of whether down is pressed.

Instead of starting a new thread for each file, I think you should start up a single thread before you start looping, and then use a Queue to pass keypresses to the main thread that you then process while looping over bio.
Reply
#7
(May-28-2017, 03:22 AM)nilamo Wrote: You call p.kill() all the time, regardless of whether down is pressed.

Instead of starting a new thread for each file, I think you should start up a single thread before you start looping, and then use a Queue to pass keypresses to the main thread that you then process while looping over bio.



Okay, I've tried to implement queue and I've simplified the code a little but, the thread is not affecting the main thread despite the conditions being met.


import time
import subprocess
import pickle
import keyboard
import threading
from threading import Thread
import multiprocessing
import queue
import time

with open('C:\\Users\Moondra\\Bioteck.pickle', 'rb') as file:
    bio = pickle.load(file)





q = queue.LifoQueue(0)


def keyboard_press():
    while True:
        q.put(keyboard.is_pressed('down'))
        x = q.get()
        print(x)
        if x == True:
           time.sleep(20)

            #keyboard.wait('esc')
        
    






t = Thread(target = keyboard_press, args= ())
t.start()


if __name__ == "__main__":

    for i in bio[:5]:
        p = subprocess.Popen(["C:\Program Files\IrfanView\i_view64.exe",'C:\\Users\Moondra\\Bioteck_charts\{}.png'.format(i)])
        
        time.sleep(3)
        p.kill()
So if the condition is met, I want to freeze the main process by 20 seconds (time.sleep(20)).
The condition is being met, because 'True' is being outputted by the print statement.
But the main thread is not responding to the thread. Thank you.
Reply
#8
(May-29-2017, 04:00 PM)bigmit37 Wrote: But the main thread is not responding to the thread. Thank you.
The whole reason to use a queue, is so the main thread can read from it while the thread writes. If you sleep the thread, you only sleep the thread, not the main thread.

So try this:
from threading import Thread
import queue
import time
import sys


def _find_getch():
    try:
        import termios
    except ImportError:
        # Non-POSIX. Return msvcrt's (Windows') getch.
        import msvcrt
        return msvcrt.getch

    # POSIX system. Create and return a getch that manipulates the tty.
    import tty

    def _getch():
        fd = sys.stdin.fileno()
        old_settings = termios.tcgetattr(fd)
        try:
            tty.setraw(fd)
            ch = sys.stdin.read(1)
        finally:
            termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
        return ch
    return _getch


getch = _find_getch()


def keyboard_press(q):
    while True:
        key = getch()
        if key == b" ":
            q.put("down")

        if not q.empty():
            val = q.get()
            if val == "done":
                break
            # if we don't process the event, put it back in the queue
            q.put(val)


if __name__ == "__main__":
    q = queue.Queue()
    t = Thread(target=keyboard_press, args=(q, ))
    t.start()

    for i in range(50):
        print(i)
        time.sleep(0.01)

        delayed = True
        while delayed:
            delayed = False
            if not q.empty():
                val = q.get()
                if val == "down":
                    print("waiting...")
                    delayed = True
                    time.sleep(5)
    q.put("done")
    t.join()
I got rid of the subprocess module, and the file loading, to simplify the problem to help show it. I also replaced the keyboard module, since it wasn't working on my machine.
Reply
#9
(May-29-2017, 04:33 PM)nilamo Wrote:
(May-29-2017, 04:00 PM)bigmit37 Wrote: But the main thread is not responding to the thread. Thank you.
The whole reason to use a queue, is so the main thread can read from it while the thread writes.  If you sleep the thread, you only sleep the thread, not the main thread.

So try this:
from threading import Thread
import queue
import time
import sys


def _find_getch():
    try:
        import termios
    except ImportError:
        # Non-POSIX. Return msvcrt's (Windows') getch.
        import msvcrt
        return msvcrt.getch

    # POSIX system. Create and return a getch that manipulates the tty.
    import tty

    def _getch():
        fd = sys.stdin.fileno()
        old_settings = termios.tcgetattr(fd)
        try:
            tty.setraw(fd)
            ch = sys.stdin.read(1)
        finally:
            termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
        return ch
    return _getch


getch = _find_getch()


def keyboard_press(q):
    while True:
        key = getch()
        if key == b" ":
            q.put("down")

        if not q.empty():
            val = q.get()
            if val == "done":
                break
            # if we don't process the event, put it back in the queue
            q.put(val)


if __name__ == "__main__":
    q = queue.Queue()
    t = Thread(target=keyboard_press, args=(q, ))
    t.start()

    for i in range(50):
        print(i)
        time.sleep(0.01)

        delayed = True
        while delayed:
            delayed = False
            if not q.empty():
                val = q.get()
                if val == "down":
                    print("waiting...")
                    delayed = True
                    time.sleep(5)
    q.put("done")
    t.join()
I got rid of the subprocess module, and the file loading, to simplify the problem to help show it.  I also replaced the keyboard module, since it wasn't working on my machine.



Thank you.
For some reason, pressing 'down' (even tried pressing continously) is not evoking the time.sleep(5).
I'm going over your code, I just want to make sure I fully understand it.

It seems you are only putting into the queue, 'down' keypresses as opposed to all key presses.
I'm not quite sure why you are putting the value back into the queue in the last line of this loop:

if not q.empty():
            val = q.get()
            if val == "done":
                break
            # if we don't process the event, put it back in the queue
            q.put(val)
When we get the value, won't that always trigger the time.sleep(5)?


Another thing I'm trying to wrap my head around the code is, it seems you are queing my the keypresses, and then unpacking their values within the main loop to trigger time.sleep(5). If I had pressed 'down' twice instead of once, would that mean the while loop would run twice before breaking out of the while loop and going back into the outer for loop?


while delayed:
            delayed = False
            if not q.empty():
                val = q.get()
                if val == "down":
                    print("waiting...")
                    delayed = True
                    time.sleep(5)
Last question, is despite the two process running in parallel, it seems I can't affect the flow of the main thread in real-time (for example, the exact second one thread receives a keypress 'down', I would like do something to the mainthread in realtime). From your code, it seems I'm limited to unpacking my side thread values somewhere in my main thread and that is the only point I can affect the main thread. Is this a limitation of programming?
Reply
#10
You need some sort of communication between threads for the different threads to interact with each other.  A thread running in the background can't just interrupt the main thread, and there isn't really a reason you'd want your code to arbitrarily start doing things you didn't want it to do.  If you want it to stop, you need to specify when and how it stops.

The reason there's some oddities with the queue, is because pushing the "done" message to the secondary thread was... probably not the right choice on my end.  But the reason for putting the message back into the queue after pulling it out if it isn't the "done" message, is so the main thread can see the button press.  Once a message is taken out of the queue, it's out of the queue for all threads, so we had to put it back in for the main thread.  Which is a little backward, and, as you saw, a pretty clear indication that there's a better option.  And that option is probably another queue, one for each direction of communication.

As to why the "down" key wasn't working, I guess I didn't realize that's what you were going for, because I had it pausing when the space bar was hit.

So here's a version that uses two different queues, to avoid wacky things like re-adding items.  And also with the down key working to pause it.
from threading import Thread
import queue
import time
import sys


def _find_getch():
   try:
       import termios
   except ImportError:
       # Non-POSIX. Return msvcrt's (Windows') getch.
       import msvcrt
       return msvcrt.getch

   # POSIX system. Create and return a getch that manipulates the tty.
   import tty

   def _getch():
       fd = sys.stdin.fileno()
       old_settings = termios.tcgetattr(fd)
       try:
           tty.setraw(fd)
           ch = sys.stdin.read(1)
       finally:
           termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
       return ch
   return _getch


getch = _find_getch()


def keyboard_press(keys, control):
   while True:
       key = getch()
       if key == b"\xe0":
           # a "control key" was hit, call again to get the actual key
           key = getch()
           # P == Down, K == Left, H == Up, M == Right,
           # I have no idea if this works the same way on linux
           if key == b"P":
               keys.put("pause")

       if not control.empty():
           val = control.get()
           if val == "stop":
               break


if __name__ == "__main__":
   key_presses = queue.Queue()
   control_messages = queue.Queue()
   t = Thread(target=keyboard_press, args=(key_presses, control_messages))
   t.start()

   for i in range(50):
       print(i)
       time.sleep(0.01)

       delayed = True
       while delayed:
           delayed = False
           if not key_presses.empty():
               val = key_presses.get()
               if val == "pause":
                   print("waiting...")
                   delayed = True
                   time.sleep(5)
   control_messages.put("stop")
   t.join()
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  I want to create one resume parser api using Python Flask parthsukhadiya 1 857 Nov-28-2023, 05:07 PM
Last Post: Pedroski55
  Pausing and returning to function? wallgraffiti 1 2,167 Apr-29-2021, 05:30 PM
Last Post: bowlofred
Question resume file transfer onran 0 1,643 Jan-27-2021, 02:16 PM
Last Post: onran
  Slide show with mouse click pausing aantono 1 2,216 Jan-28-2020, 04:25 AM
Last Post: Larz60+
  Pausing a running process? MuntyScruntfundle 2 7,268 Nov-14-2018, 05:14 PM
Last Post: woooee
  Pausing a Script malonn 4 3,830 Aug-04-2018, 07:58 PM
Last Post: malonn
  How to Make Python code execution pause and resume to create .csv and read values. Kashi 2 3,777 Jun-14-2018, 04:16 PM
Last Post: DeaD_EyE
  Pausing and playing a while loop. traceon 1 3,891 Jan-24-2017, 09:11 PM
Last Post: wavic

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020