shayan
shayan

Reputation: 101

How can I preserve program reactivity inside a reactive function?

I am working on a python-based shiny app that serves to drive a fluid pump over a serial transmission line. After configuring a flow profile with set amplitudes and runtime, the serial transmission can be started by pressing an action button "p1".

The issue I am facing is the lack of reactivity inside the reactive.event(input.p1) associated with the button "p1": I want to ensure that the transmission started by clicking "p1" thus triggering the reactive.event(input.p1) can be terminated anytime by clicking "p2".

However, the current implementation causes the stop message in reactive.event(input.p2) to queue up and be sent after the transmission in reactive.event(input.p1) has ended.

How can I fix this? Is there any way to make sure my program is still reactive to other inputs? I want the transmission to stop immediately as soon as "p2" is clicked. Implementation of both buttons are dropped below.

@reactive.Effect
@reactive.event(input.p1)
def _():
    y = yg.get() # fetch np.array y from reactive value yg, numbers in y correspond to driving voltages

     for e in y: # iterate over array
         msg = "1:1:"+str(e)+":100" # formatted string containing the driving voltage
         #print(msg) # print to console in debug mode
         ser.write(bytes(msg,'utf-8')) # send the formatted string
         t0 = time.time() # time stamp
        
         while(((time.time()-t0)<=2)): # next driving voltage should be transmitted after 2 seconds
             pass
    ser.write(bytes("0:1",'utf-8')) # stops the pump after transmission has ended

@reactive.Effect
@reactive.event(input.p2)
def _():
    #print("1:0") 
    ser.write(bytes("0:1",'utf-8')) # Stop the pump

Upvotes: 5

Views: 434

Answers (1)

shayan
shayan

Reputation: 101

Alright, I managed to solve the problem by creating a timer through threading. Personally, I think that the answer is that in and of itself it is not possible to preserve reactivitiy inside a loop. I tried various implementations including the ones that @phili_b suggested. But nothing really forced the program out of the while loop that was called in a reactive event related to button p1 by clicking button p2.

This is the solution that worked for me:

 # This function may very well be included into the one below, 
 # but in this case this structure serves me well
 # function takes a number, forms the message string and puts it on the serial port
 def transmit(e): 
         msg = "1:1:"+str(e)+":100"
         #print(msg)
         ser.write(bytes(msg,'utf-8'))

 # This is the main function that is being threaded. Loop iterates over array y
 # every 2 seconds until either end of y or the threading Event sflag is triggered
    def rtimer(y,sflag):   # Timer calls this function    
        i = 0
        while i<np.size(y) and not sflag.is_set():
            transmit(y[i])
            i+=1
            time.sleep(2)  # 2 second interval between transmissions

# p1-button calls this function
    @reactive.Effect()
    @reactive.event(input.p1)
    def  _():
        y = yg.get()               
        sflag.clear() # clear slfag in case it has been triggered prior
        timer_thread = th.Thread(target=rtimer,args=[y,sflag]) # threading is imported as th
        timer_thread.start() 

# p2-button calls this function
    @reactive.Effect()
    @reactive.event(input.p2)
    def stop():
        #print("1:0")
        sflag.set()
        ser.write(bytes("1:0",'utf-8'))

I know this is a workaround rather than a fix for the implementation I suggested in the OP. Although the code now does what I want it to do, I am still curious if there is a way to make the original code work. I am interested to know if it is intrinsically possible to deal with the limitations a loop seemingly has on the program.

Edit

Added entire code per request: Note that the buttons are found on the bottom left. GUI is in a very rough state and looks hideous. This is just for reproduction purposes.

from shiny import App, render, ui, reactive
import serial
import time
import matplotlib.pyplot as plt
import numpy as np
import shinyswatch
import threading as th
import asyncio
#import reprex
# class RepeatTimer(th.Timer):
#     def run(self):
#          while not self.finished.wait(self.interval):  
#             self.function(*self.args,**self.kwargs)         

#x = np.empty(1)
#y = np.empty(1)

# 
# Pump PowerMode Amplitude Frequenz Amplitude2
# Pump
# 1 Erste Pumpe
# 2 Zweite Pumpe
# 6 Einschalten beider Pumpen
# 9 Ausschalten beider Pumpen
# powerMode
# 0 Ausschalten der Pumpe
# 1 Einschalten der Pumpe
# Amplitude/Amplitude2
# Ganzzahl zwischen 0-250 Amplitudenwert in Vpp
# Frequenz
# 100 Festgelegte Frequenz von 100Hz
# Format
# 1 : 1 : 250 : 100 : 150
# Pump : PowerMode : Amplitude : Frequenz : Amplitude2
ser = serial.Serial("COM6",115200)
#print(ser.name)
app_ui = ui.page_fluid(
     shinyswatch.theme.minty(),
    ui.row(

        ui.column(8,
            
            ui.h2("Header 1"),
            ui.row(
                ui.column(2,
                ui.h5("Profile"),
                ui.input_radio_buttons("M1","",{"A":"Constant","B":"Linear","C":"Periodic"}) 
                          ),
                ui.column(2,ui.h5("Parameter"),
                          ui.panel_conditional("input.M1 === 'A'", 
                                               ui.input_numeric("AK","Amplitude [V]",value=100),ui.input_numeric("TK","Runtime [s]",value=10)),
                          ui.panel_conditional("input.M1 === 'B'", 
                                               ui.input_switch("lin","Rising",True),ui.input_numeric("SL","Start-Amplitude [V]",value=50,min=10,max=250),ui.input_numeric("EL","End-Amplitude [V]",value=100,min=10,max=250),ui.input_numeric("TL","Runtime [s]",value=10,min=1)),
                          ui.panel_conditional("input.M1 === 'C'", 
                                               ui.input_switch("per","Sine",True),ui.input_numeric("PMIN","Min-Amplitude [V]",value=50,min=10,max=250),ui.input_numeric("PMAX","Max-Amplitude [V]",value=100,min=10,max=250),ui.input_numeric("TP","Runtime [s]",value=10,min=1))
                          ),
                ui.column(8,ui.output_plot("preview1")
                          )       
                )),
    
    
        ui.column(4,
              
            ui.h2("test"),
            ui.input_slider("n", "N", 20, 60, 20),
            ui.output_text_verbatim("txt"),
            ui.input_action_button("b1","Plot", class_="btn-success"),
            ui.output_plot("pl"),
            ui.output_text_verbatim("t2"),
            ui.input_action_button("b2","Pumpe Start"),
            ui.input_action_button("b3","Pumpe Stopp"), 
                )
        ), 

    ui.row(
    
    ui.input_radio_buttons("PA","",{"Pu1":"Pumpe 1","Pu2":"Pumpe 2","Pu3":"Pumpe 3","Pu4":"Pumpe 4"}) 
                          ,

    ui.column(3,
            ui.input_action_button("p1","Pumpe Start"),
            ui.input_action_button("p2","Pumpe Stopp"),
            ui.input_action_button("t","Test")    
            )

    )  
)


def server(input, output, session):
    status = reactive.Value("Pumpe aus")
    xg = reactive.Value()
    yg = reactive.Value()
    #yg1 = 
    sflag = th.Event()

    def transmit(e):
         msg = "1:1:"+str(e)+":100"
         #print(msg)
         ser.write(bytes(msg,'utf-8'))

    def rtimer(y,sflag):       
        i = 0
        while i<np.size(y) and not sflag.is_set():
            transmit(y[i])
            i+=1
            time.sleep(2)

    @reactive.Effect()
    @reactive.event(input.p1)
    def  _():
        y = yg.get()    
       
        sflag.clear()
        timer_thread = th.Thread(target=rtimer,args=[y,sflag])
        timer_thread.start()          
        # for i in range(np.size(y)):
        #  t1 = th.Timer(2,transmit,args = [i,y])
        #  t1.start()
        #timer = RepeatTimer(2,transmit,[1,y])
        #timer.start()       
    #    for e in range(0,20):
    #        test(i,y)
    #     x = xg.get()
    #     y = yg.get()              
    #     ind = 0        
        #for e in y:
            # task = asyncio.create_task(st()) 
            # msg = "1:1:"+str(e)+":100"
            # print(msg)
    #       # ser.write(bytes(msg,'utf-8'))         
           #  await asyncio.sleep(2)
                                   
    @reactive.Effect 
    @reactive.event(input.t)
    def _():
        msg = "1:1:"+str(80)+":100"
        print(msg)
        #ser.write(bytes(msg,'utf-8'))
    #@reactive.event(input.t)
    #def _():
    @reactive.Effect()
    @reactive.event(input.p2)
    def stop():
        #print("1:0")
        sflag.set()
        ser.write(bytes("1:0",'utf-8'))
    @reactive.Effect
    @reactive.event(input.per)
    def _():
        
        if(input.per()):
          ui.update_switch(
          "per",label="Sine"
        )
        else:
           ui.update_switch(
          "per",label="Cosine"
        )

    @reactive.Effect
    @reactive.event(input.lin)
    def _():
        
        if(input.lin()):
          ui.update_switch(
          "lin",label="Rising"
        )
        else:
           ui.update_switch(
          "lin",label="Falling"
        )   
            
    @output
    @render.text
    def txt():
        #ser.write(b"input.n()")
        #time.sleep(5)
        return f"{input.n()} V pro Zeitschritt"
    @output
    @render.plot
    @reactive.event(input.b1,ignore_none=False)
    def pl():
        px = np.arange(0,100,1) 
        py = np.arange(100,200,1)
        fig, ax = plt.subplots()
        ax.plot(px,py)
        plt.xlabel("Zeit [s]")
        plt.ylabel("Amplitude [V]")
        return fig
    
    @reactive.Effect
    @reactive.event(input.b2)
    def _():
        #ser.write(b"1:1:150:100")
        #ser.write(b"1:2")
        status.set("P1 On")
        
    @reactive.Effect
    @reactive.event(input.b3)
    def _():
        #ser.write(b"0:1")
        print("0:1")
        status.set("P1 Off")
    @output
    @render.text
    def t2():
        return str(status())
    
    @output
    @render.plot
    @reactive.event(input.AK,input.TK,input.SL,input.EL,input.TL,input.PMIN,input.PMAX,input.TP,input.M1,input.per)
    def preview1():
        match input.M1():
            case "A":
                 x = np.arange(0,input.TK()+2,2)            
                 y = np.ones(np.size(x))*input.AK()   
                 fig, ax = plt.subplots()
                 ax.plot(x,y)
                 plt.title("Constant Profile")
                 plt.xlabel("Time [s]")
                 plt.ylabel("Amplitude [V]")
                 xg.set(np.rint(x).astype(int))
                 yg.set(np.rint(y).astype(int))
                 return fig
            case "B":
                 x = np.arange(0,input.TL()+2,2)        
                 
                 #if input.
                 y = x*((input.EL()-input.SL())/input.TL()) + input.SL()
                 fig, ax = plt.subplots()
                 ax.plot(x,y)
                 plt.title("Linear Profile")
                 plt.xlabel("Time [s]")
                 plt.ylabel("Amplitude [V]")
                 xg.set(np.rint(x).astype(int))
                 yg.set(np.rint(y).astype(int))
                 return fig
            case "C":
                x = np.arange(0,input.TP(),0.1)
                if(input.per()):
                    y = (0.5*(input.PMAX()-input.PMIN())) * np.sin(x) + (np.mean([input.PMAX(),input.PMIN()]))      
                else:
                    y = (0.5*(input.PMAX()-input.PMIN())) * np.cos(x) + (np.mean([input.PMAX(),input.PMIN()]))
                fig, ax = plt.subplots()
                ax.plot(x,y)
                plt.title("Periodic Profile")
                plt.xlabel("Time [s]")
                plt.ylabel("Amplitude [V]") 
                xg.set(np.rint(x).astype(int))
                yg.set(np.rint(y).astype(int))
                return fig

app = App(app_ui, server)

Upvotes: 0

Related Questions