Reputation: 101
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
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.
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