3

I want to:

  • Read from serial port (infinite loop)
  • when "STOP" button pressed --> Stop reading and plot data

From How to kill a while loop with a keystroke? I have taken the example to interrupt using Keyboard Interrupt, This works, but i would like to use a button.

EXAMPLE WITH KEYBOARD INTERRUPT

weights = []
times = [] 
#open port 
ser = serial.Serial('COM3', 9600)
try:
   while True: # read infinite loop
       #DO STUFF
       line = ser.readline()   # read a byte string
       if line:
           weight_ = float(line.decode())  # convert the byte string to a unicode string
           time_ = time.time()
           weights.append(weight_)
           times.append(time_)
           print (weight_)
#STOP it by keyboard interup and continue with program 
except KeyboardInterrupt:
   pass
#Continue with plotting

However I would like to do it with a displayed button (easier for people to use). I have tried making a button (in Jupiter Notebook) that when pressed break_cicle=False, but the loop doesn't break when button pressed:

 #make a button for stopping the while loop 
button = widgets.Button(description="STOP!") #STOP WHEN THIS BUTTON IS PRESSED
output = widgets.Output()
display(button, output)
break_cicle=True


def on_button_clicked(b):
    with output:
        break_cicle = False # Change break_cicle to False
        print(break_cicle)
        
ser.close()   
button.on_click(on_button_clicked)
ser = serial.Serial('COM3', 9600)
try:
    while break_cicle:

        print (break_cicle)
        line = ser.readline()   # read a byte string
        if line:
            weight_ = float(line.decode())  # convert the byte string to a unicode string
            time_ = time.time()
            weights.append(weight_)
            times.append(time_)
            print (weight_)
except :
    pass

ser.close()    

EXAMPLE WITH GLOBAL NOT WORKING

from IPython.display import display
import ipywidgets as widgets

button = widgets.Button(description="STOP!") #STOP WHEN THIS BUTTON IS PRESSED
output = widgets.Output()
display(button, output)
break_cicle=True

def on_button_clicked():
    global break_cicle #added global
    with output:
        
        break_cicle = False # Change break_cicle to False
        print ("Button pressed inside break_cicle", break_cicle)
    
    
button.on_click(on_button_clicked)
try:
    while break_cicle:
        print ("While loop break_cicle:", break_cicle)
        time.sleep(1)
except :
    pass
print ("done")

Despite me pressing the button a few times,from the following image you can see that it never prints "Button pressed inside break_cicle".

enter image description here

Wayne
  • 6,607
  • 8
  • 36
  • 93
Leo
  • 1,176
  • 1
  • 13
  • 33
  • inside `on_button_clicked()` you have to use `global break_cicle` to inform function that you want assign value to global/external variable `break_cicle`. At this moment `on_button_clicked()` creates local variable `break_cicle` and it doesn't change value in global variable `break_cicle` – furas Feb 21 '22 at 14:05
  • @furas, I modified the code inserting a global break cicle (see EDIT ABOVE) , however it still doesn't seem to work : i never seem to go into the "with output" part – Leo Feb 22 '22 at 09:57

1 Answers1

8

I think problem is like in all Python scripts with long-running code - it runs all code in one thread and when it runs while True loop (long-running code) then it can't run other functions at the same time.

You may have to run your function in separated thread - and then main thread can execute on_button_clicked

This version works for me:

from IPython.display import display
import ipywidgets as widgets
import time
import threading

button = widgets.Button(description="STOP!") 
output = widgets.Output()

display(button, output)

break_cicle = True

def on_button_clicked(event):
    global break_cicle
    
    break_cicle = False

    print("Button pressed: break_cicle:", break_cicle)
    
button.on_click(on_button_clicked)

def function():
    while break_cicle:
        print("While loop: break_cicle:", break_cicle)
        time.sleep(1)
    print("Done")
    
threading.Thread(target=function).start()

Maybe Jupyter has some other method for this problem - ie. when you write functions with async then you can use asyncio.sleep() which lets Python to run other function when this function is sleeping.


EDIT:

Digging in internet (using Google) I found post on Jyputer forum

Interactive widgets while executing long-running cell - JupyterLab - Jupyter Community Forum

and there is link to module jupyter-ui-poll which shows similar example (while-loop + Button) and it uses events for this. When function pull() is executed (in every loop) then Jupyter can send events to widgets and it has time to execute on_click().

import time
from ipywidgets import Button
from jupyter_ui_poll import ui_events

# Set up simple GUI, button with on_click callback
# that sets ui_done=True and changes button text
ui_done = False
def on_click(btn):
    global ui_done
    ui_done = True
    btn.description = ''

btn = Button(description='Click Me')
btn.on_click(on_click)
display(btn)

# Wait for user to press the button
with ui_events() as poll:
    while ui_done is False:
        poll(10)          # React to UI events (upto 10 at a time)
        print('.', end='')
        time.sleep(0.1)
print('done')

In source code I can see it uses asyncio for this.


EDIT:

Version with multiprocessing

Processes don't share variables so it needs Queue to send information from one process to another.

Example sends message from button to function. If you would like to send message from function to button then better use second queue.

from IPython.display import display
import ipywidgets as widgets
import time
import multiprocessing

button = widgets.Button(description="STOP!") 
output = widgets.Output()

display(button, output)

queue = multiprocessing.Queue()

def on_button_clicked(event):
    queue.put('stop')
    print("Button pressed")
    
button.on_click(on_button_clicked)

def function(queue):
    
    while True:
        print("While loop")
        time.sleep(1)
        
        if not queue.empty():
            msg = queue.get()
            if msg == 'stop':
                break
            #if msg == 'other text':             
            #    ...other code...
            
    print("Done")
    
multiprocessing.Process(target=function, args=(queue,)).start()

or more similar to previous

def function(queue):

    break_cicle = True
    
    while break_cicle:
        print("While loop: break_cicle:", break_cicle)
        time.sleep(1)
        
        if (not queue.empty()) and (queue.get() == 'stop'):
            break_cicle = False
        
    print("Done")

EDIT:

Version with asyncio

Jupyter already is running asynio event loop and I add async function to this loop. And function uses await functions like asyncio.sleep so asynio event loop has time to run other functions - but if function could run only standard (not async) functions then it wouldn't work.

from IPython.display import display
import ipywidgets as widgets
import asyncio

button = widgets.Button(description="STOP!") 
output = widgets.Output()

display(button, output)

break_cicle = True

def on_button_clicked(event):
    global break_cicle
    
    break_cicle = False

    print("Button pressed: break_cicle:", break_cicle)
    
button.on_click(on_button_clicked)

async def function():   # it has to be `async`
    while break_cicle:
        print("While loop: break_cicle:", break_cicle)
        await asyncio.sleep(1)   # it needs some `await` functions
    print("Done")
    
loop = asyncio.get_event_loop()    
t = loop.create_task(function())  # assign to variable if you don't want to see `<Task ...>` in output
furas
  • 134,197
  • 12
  • 106
  • 148
  • A similar approach is possible using the multiprocessing module, see my comments [here](https://stackoverflow.com/q/71148286/8508004). – Wayne Feb 22 '22 at 17:02
  • @Wayne with mutliprocessing it would need some `Queue` to send information from `Button` to long-running `function`. I think it could be more complex. Maybe later I will try to create example – furas Feb 22 '22 at 17:19
  • 1
    @Wayne I added example which use `mutliprocessing.Process` and `mutliprocessing.Queue` – furas Feb 22 '22 at 17:42
  • @furas Interestingly, there's some slight differences in behaviors depending on what implementation is used & what Jupyter interface. The multiprocessing example says 'Done' in **both** JupyterLab and the classic notebook interface while the asyncio version only plays the 'Done' message in JupyterLab. For both of these, the button pressed notice only gets displayed in the classic notebook. – Wayne Mar 01 '22 at 21:20
  • 2
    A related asyncio solution for a simpler situation, where user wanted to end a while loop when the slider was put to a certain setting, is [posted in response to 'Is it possible to get the current value of a widget slider from a function without using multithreading?'](https://discourse.jupyter.org/t/is-it-possible-to-get-the-current-value-of-a-widget-slider-from-a-function-without-using-multithreading/15524/2?u=fomightez). A might be useful for those finding this and needing another example. – Wayne Aug 27 '22 at 22:12