I am trying to make a metronome with kivy, i got my +, -, start button and a label which refers to the tempo.
When i click start button i run:
self.event = Clock.schedule_interval(self.job, self.bpm)
to start my timer however there are some issues.
As i click the button the timer starts with tiny noticable delay as kivy.Clock relies on frames from what i read and i should be using "Free Clock" instead but i couldn't make it work and didn't understand what i should be doing.
Second issue is when i schedule a timer it starts and keeps on going with its initial interval value, changing bpm won't change the current timer's interval until i cancel and restart it.
I would appreciate your ideas to find a way around this. My Code:
from kivy.app import App
from kivy.uix.floatlayout import FloatLayout
from kivy.lang import Builder
from kivy.core.window import Window
from kivy.properties import StringProperty
from kivy.clock import Clock
Window.size = 320, 568
kv = Builder.load_file("test.kv")
class mainWindow(FloatLayout):
tempo = 60
bpm = 60/tempo
labelTempo = StringProperty(str(tempo))
interval = False
def startTimer(self, state):
if state == "startMtr" and self.interval == False:
self.event = Clock.schedule_interval(self.job, self.bpm)
elif state == "stop":
self.event.cancel()
def button(self, btn):
if btn == "+":
self.tempo += 1
self.labelTempo = str(self.tempo)
self.bpm = 60 / self.tempo
if btn == "-":
self.tempo -= 1
self.labelTempo = str(self.tempo)
self.bpm = 60 / self.tempo
if btn == "start":
self.startTimer("startMtr")
print("started")
if btn == "stop":
self.startTimer("stop")
print("stopped")
abc = 0
def job(self, dt):
self.abc += 1
print(self.abc)
print(App.get_running_app().root.bpm)
print(App.get_running_app().root.tempo)
class crApp(App):
def build(self):
return mainWindow()
if __name__ == '__main__':
crApp().run()
Kv file:
<mainWindow>:
FloatLayout:
minus: minus
start: start
plus: plus
Button:
id: minus
text: "-"
size_hint: 0.3, 0.25
pos_hint: {"x": 0.0, "top": 0.4}
on_release:
root.button("-")
ToggleButton:
id: start
text: "start"
size_hint: 0.4, 0.25
pos_hint: {"x": 0.3, "top": 0.4}
on_press:
root.button("start") if start.state == "down" else root.button("stop")
Button:
id: plus
text: "+"
size_hint: 0.3, 0.25
pos_hint: {"x": 0.7, "top": 0.4}
on_release:
root.button("+")
Label:
id: tempo
text: root.labelTempo
From the kivy docs:
The default clock suffers from the quantization problem, as frames occur only on intervals and any scheduled timeouts will not be able to occur during an interval.
So to get around that, use the "free clock" by
Selecting a clock mode using the config:
# set config options before other imports
from kivy.config import Config
Config.set('kivy', 'kivy_clock', 'free_all')
Where there are four options:
When kivy_clock is default, the normal clock, ClockBase, which limits
callbacks to the maxfps quantization - is used.
When kivy_clock is interrupt, a interruptible clock,
ClockBaseInterrupt, which doesn’t limit any callbacks to the maxfps -
is used. Callbacks will be executed at any time.
When kivy_clock is free_all, a interruptible clock,
ClockBaseFreeInterruptAll, which doesn’t limit any callbacks to the
maxfps in the presence of free events, but in their absence it limits
events to the fps quantization interval - is used.
When kivy_clock is free_only, a interruptible clock,
ClockBaseFreeInterruptAll, which treats free and normal events
independently; normal events are fps limited while free events are not
is used.
call the _free method version:
Clock.schedule_interval_free(self.job, self.bpm)
Related
I'm working on a program with a GUI interface, which I've implemented using a kivy app. My end-goal is to enter a desired voltage in the kivy textbox and for it to be set using an Arduino + a DAC converter. For now I'm just trying to access the voltage from the text input from outside the kivy app in my main, so that I can separate the GUI cleanly from communication with the arduino, and other necessary calculations.
The problem is that the program doesn't continue after App.run() until the kivy app has been closed and the entered voltage has been lost. I've tried using the multiprocessing library, but the start() function also runs as long as the app is running. I'm also not sure what the best way is to access the voltage, I tried having it as a class member of the GUI, but maybe that's not a good idea.
This is my GUI.py code:
from kivy.app import App
from kivy.uix.label import Label
from kivy.uix.gridlayout import GridLayout
from kivy.uix.textinput import TextInput
from kivy.properties import StringProperty, NumericProperty
from kivy.uix.button import Button
class GUI_GridLayout(GridLayout):
voltage_label_1 = StringProperty("Voltage 1?")
voltage_label_2 = StringProperty("Voltage 2?")
voltage_input_1 = TextInput()
voltage_input_2 = TextInput()
submit_button_label_1 = StringProperty("Submit 1")
submit_button_label_2 = StringProperty("Submit 2")
voltage_output_1 = NumericProperty(0)
voltage_output_2 = NumericProperty(0)
def press_1(self):
voltage_1 = self.voltage_input_1.text
self.voltage_output_1 = float(voltage_1)
def press_2(self):
voltage_2 = self.voltage_input_2.text
self.voltage_output_2 = float(voltage_2)
def build(self):
self.add_widget(Label(text=self.voltage_label_1))
self.add_widget(self.voltage_input_1)
self.add_widget(Label(text=self.voltage_label_2))
self.add_widget(self.voltage_input_2)
self.submit_button_1 = Button(text=self.submit_button_label_1)
self.submit_button_1.bind(on_press=self.press_1)
self.add_widget(self.submit_button_1)
self.submit_button_2 = Button(text=self.submit_button_label_2)
self.submit_button_2.bind(on_press=self.press_2)
self.add_widget(self.submit_button_2)
class apason_GUIApp(App):
def build(self):
return GUI_GridLayout()
The corresponding kv file:
#:kivy 1.0.9
<GUI_GridLayout>:
cols: 3
voltage_input_1: input_1
voltage_input_2: input_2
Label:
font_size: 50
text: str(root.voltage_label_1)
TextInput:
id: input_1
font_size: 50
multiline: False
text: root.voltage_input_1.text
Button:
font_size: 50
text: str(root.submit_button_label_1)
on_press: root.press_1()
Label:
font_size: 50
center_x: root.width / 4
text: str(root.voltage_label_2)
TextInput:
id: input_2
font_size: 50
multiline: False
text: root.voltage_input_2.text
Button:
font_size: 50
text: str(root.submit_button_label_2)
on_press: root.press_2()
And here's my main:
import GUI.GUI as gui
import multiprocessing as multiproc
import time
class GetVoltage:
def __init__(self):
self.voltage_1 = 0
self.voltage_2 = 0
def fetchVoltage(self, interface):
self.voltage_1 = interface.voltage_output_1
self.voltage_2 = interface.voltage_output_2
def run(self, interface):
while (True):
self.fetchVoltage(interface)
print(self.voltage_1)
print(self.voltage_2)
time.sleep(1)
if __name__ == '__main__':
interface = gui.apason_GUIApp()
interface_process = multiproc.Process(target=interface.run())
checker = GetVoltage()
checker_process = multiproc.Process(target=checker.run(interface))
interface_process.start()
checker_process.start()
Since you already have one process when you run main.py, you really only need to start the second process. And you can communicate between the processes using a Queue. Here is a modified version of your main.py that uses this approach:
import GUI as gui
import multiprocessing as multiproc
class GetVoltage:
def run(self, queue):
while True:
voltage = queue.get() # get the data from the other process by using the Queue
print(voltage)
if __name__ == '__main__':
q = multiproc.Queue()
# start the GetVoltage process
checker = GetVoltage()
checker_process = multiproc.Process(target=checker.run, args=(q,), daemon=True)
checker_process.start()
# start the App
interface = gui.apason_GUIApp()
interface.q = q
interface.run() # this does not return until the App closes
# kill the GetVoltage process after the App exits
checker_process.kill()
And a couple minor changes in the App code:
def press_1(self):
voltage_1 = self.voltage_input_1.text
self.voltage_output_1 = float(voltage_1)
App.get_running_app().q.put('voltage_1 = ' + voltage_1) # put the data in the Queue
def press_2(self):
voltage_2 = self.voltage_input_2.text
self.voltage_output_2 = float(voltage_2)
App.get_running_app().q.put('voltage_2 = ' + voltage_2) # put the data in the Queue
I have created a simple program that demonstrates my current dilemma. Whenever I press button 2 on the kivy GUI I would like to increment a variable. After this variable is incremented I would like to create a thread that runs separately to do something to the value of the variable. For this instance just adding a sleep of 5 seconds before printing it back will be fine. I have to use threading because any use of sleep or interrupts inside gui, stops it from working during that sleep or kills it completely.
You might need to install the kivy lib for the kivy application to work on windows
import threading
from kivy.config import Config
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.clock import Clock
from kivy.lang import Builder
from kivy.properties import StringProperty
from datetime import datetime
#import adafruit_rfm9x
#from gpiozero import MotionSensor
#import sys
import time
#from gpiozero import MotionSensor
import subprocess
global buttonVal
# Setting up size of window for kivy app
Config.set('graphics', 'resizable', '0')
# fix the width of the window
Config.set('graphics', 'width', '800')
# fix the height of the window
Config.set('graphics', 'height', '480')
# Kivy app dependencies, buttons, labels, etc
Builder.load_string("""
<MySec>:
orientation: 'vertical'
size: root.width, root.height
rotation: 90
Label:
text: 'Master Wall Mount'
font_size: 45
Button:
id: kv_sec
text: root.seconds_string
font_size: 80
Button:
id: button2
text: 'Button 2'
on_press: root.button_toggle()
Button:
id: button3
text: 'Button 3'
""")
# function to turn on screen
def turn_on():
CONTROL = "vcgencmd"
CONTROL_UNBLANK = [CONTROL, "display_power", "1"]
subprocess.call(CONTROL_UNBLANK)
# function to turn off screen
def turn_off():
CONTROL = "vcgencmd"
CONTROL_BLANK = [CONTROL, "display_power", "0"]
subprocess.call(CONTROL_BLANK)
# function that will run as a thread to run the pir sensor infinitely to determine if motion is present and proceed
# to adjust screen brightness.
def pirsensor():
# Setting GPIO pin 4 as pir sensor
pir = MotionSensor(4)
# dhtDevice = adafruit_dht.DHT22(board.D23)
while True:
if pir.motion_detected:
turn_on()
print("Motion Detected!")
time.sleep(120.0)
print("Sleeping for 2 minutes")
else:
turn_off()
print("No Motion Detected!")
def send_button_data():
print(buttonVal)
# pirsenor thread using pirsensor function
t1 = threading.Thread(target=pirsensor)
t2 = threading.Thread(target=send_button_data())
class MySec(BoxLayout):
seconds_string = StringProperty('')
count = 0
def button_toggle(self):
self.count += 1
print(self.count)
buttonVal = self.count
class TestThreadApp(App):
print("Running Thread for PIR Sensor")
t1.start()
print("Running Thread to display current button value" )
t2.start()
def build(self):
Clock.schedule_interval(lambda dt: self.update_time(), 1)
return MySec()
def update_time(self):
self.root.seconds_string = datetime.now().strftime("%H:%M:%S")
TestThreadApp().run()
I am trying to have a function run continuously and spit out a distance that the label uses that I'll eventually tie to a sonar module, but the label remains blank and I am at a loss as to what I am doing wrong. If I just add a print statement for that distance variable it prints and updates just fine, just can't get the label to use it.
Part II of my question is how do I reference my same function in the second window and also have a label that updates from that same function?
Thanks for the help in advance, I am very very new to kivy and just started learning python a few months ago as well.
Python code:
from kivy.app import App
from kivy.clock import Clock
from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager, Screen # for multiple screens
from kivy.properties import StringProperty
class MySonar(Screen):
global i
i = 1
distance = StringProperty("")
#Generic function that just adds itself up, just using to try and get the label to change before I throw in my real function
def sonar(self):
global i
if i < 250:
distance = (10 + .1 * i)
i += 1
else:
i = 1
distance = 10
self.root.distance=str(distance)
class DropWindow(Screen):
pass
class WindowManager(ScreenManager):
pass
kv = Builder.load_file("help.kv")
class HelpMe(App):
def build(self):
#running interval update to keep running code above
Clock.schedule_interval(lambda dt: MySonar.sonar(self), 0.1)
return kv
if __name__ == "__main__":
HelpMe().run()
Kivy:
WindowManager:
MySonar:
DropWindow:
<MySonar>:
name:"Main"
GridLayout:
cols:1
##Need this to update
Label:
text:root.distance
Button:
text:"Next Window"
on_release:
app.root.current="Drop"
root.manager.transition.direction="left"
<DropWindow>:
name:"Drop"
GridLayout:
cols:1
##Need this to update, commented out the text so the program will run and you can see the blank label for part one of my question
Label:
##text:root.distance
Button:
text:"Cancel"
on_release:
app.root.current="Main"
root.manager.transition.direction="right"
To simplify the access to distance, you can put that StringProperty in the HelpMe App:
class MySonar(Screen):
global i
i = 1
#Generic function that just adds itself up, just using to try and get the label to change before I throw in my real function
def sonar(self):
global i
if i < 250:
distance = (10 + .1 * i)
i += 1
else:
i = 1
distance = 10
# set the value of distance in the StringProperty of the App
App.get_running_app().distance=str(distance)
print(distance)
class DropWindow(Screen):
pass
class WindowManager(ScreenManager):
pass
# kv = Builder.load_file("help.kv")
class HelpMe(App):
distance = StringProperty('')
def build(self):
kv = Builder.load_file("help.kv")
#running interval update to keep running code above
sonar_instance = kv.get_screen('Main')
Clock.schedule_interval(lambda dt: sonar_instance.sonar(), 0.1)
return kv
if __name__ == "__main__":
HelpMe().run()
Note that I have also moved the Builder.load_file() inside the App. This is good practice when you reference app in the kv file (as I have done). Also, calling the sonar() method using MySonar.sonar(self) will not work. You need to use a reference to the instance of MySonar that is in your GUI.
Now the kv file becomes:
WindowManager:
MySonar:
DropWindow:
<MySonar>:
name:"Main"
GridLayout:
cols:1
##Need this to update
Label:
text: app.distance
Button:
text:"Next Window"
on_release:
app.root.current="Drop"
root.manager.transition.direction="left"
<DropWindow>:
name:"Drop"
GridLayout:
cols:1
##Need this to update, commented out the text so the program will run and you can see the blank label for part one of my question
Label:
text: app.distance
Button:
text:"Cancel"
on_release:
app.root.current="Main"
root.manager.transition.direction="right"
The change is that the text attribute of both Labels is now just app.distance.
About issue: In sutPopUp.create(), I am trying to get the popup to update itself (removing it's two buttons and change the "hidden" label). I have tried both threading the update function and calling Clock.schedule_once(self.update_txt, -1), but neither one has worked. They both seemingly wait for run_local_command, which is just a blocking function that runs a local command. Below is the python code:
class sutPopUp(Popup):
pop_float = ObjectProperty(None)
sutPopUp_create = ObjectProperty(None)
sutPopUp_cancel = ObjectProperty(None)
sut_wait_text = ObjectProperty(None)
sutPopUp_input = ObjectProperty(None)
def __init__(self, my_widget,**kwargs):
super(sutPopUp,self).__init__(**kwargs)
self.title = "Test Station Setup"
self.size_hint = (None, None)
self.size = (400, 200)
def create(self, *args):
self.quick_change_thread = threading.Thread(target=self.update_txt)
self.quick_change_thread.start()
time.sleep(1)
sut_name = self.sutPopUp_input.text
create_cmd = "python project.py -c " + sut_name
create_handle = run_local_command(create_cmd, True, "C:\Project")
wm.current = "blank"
wm.remove_widget(screens[2])
screens[2] = Dashboard(name="dashboard_screen")
wm.add_widget(screens[2])
wm.current = "dashboard_screen"
self.dismiss()
self.quick_change_thread.join()
def update_txt(self):
self.sutPopUp_create.disabled = True
self.sutPopUp_cancel.disabled = True
self.pop_float.remove_widget(self.sutPopUp_create)
self.pop_float.remove_widget(self.sutPopUp_cancel)
self.sut_wait_text.text = "Creating Test Station ..."
Here is the kv:
<sutPopUp#Popup>
pop_float:pop_float
sutPopUp_create:sutPopUp_create
sutPopUp_cancel:sutPopUp_cancel
sut_wait_text:sut_wait_text
sutPopUp_input:sutPopUp_input
FloatLayout:
id: pop_float
size: root.height, root.width
Label:
text: "Enter Test Station Name:"
pos_hint:{"x":0.1,"top":0.9}
size_hint: 0.25, 0.2
TextInput:
id: sutPopUp_input
multiline: False
pos_hint:{"x":0,"top":0.7}
size_hint: 1, 0.2
Label:
id: sut_wait_text
text: ""
pos_hint:{"x":0.4,"top":0.3}
size_hint: 0.25, 0.2
Button:
id: sutPopUp_create
text: "Create"
pos_hint:{"x":0.1,"top":0.3}
size_hint: 0.3, 0.2
on_release: root.create()
Button:
id: sutPopUp_cancel
text: "Cancel"
pos_hint:{"x":0.6,"top":0.3}
size_hint: 0.3, 0.2
on_release: root.dismiss()
I suggest breaking the create() method into three methods. Since the create() method is initiated by a Button press, it will run on the main thread, so simply call the update_text() method directly. Then use another thread to run the create_cmd. That new thread then uses Clock.schedule_once() to run the remaining code from the original create() method (the new finish_create() method) back on the main thread.
def create(self, *args):
self.update_text() # this needs to run on the main thread
# time.sleep(1) # ??? this will freeze your app for 1 second
Thread(target=self.do_create).start()
def do_create(self):
sut_name = self.sutPopUp_input.text
create_cmd = "python project.py -c " + sut_name
create_handle = run_local_command(create_cmd, True, "C:\Project")
# after above command completes, run the rest of the former create() back on the main thread
Clock.schedule_once(self.finish_create)
def finish_create(self, dt):
wm.current = "blank"
wm.remove_widget(screens[2])
screens[2] = Dashboard(name="dashboard_screen")
wm.add_widget(screens[2])
wm.current = "dashboard_screen"
self.dismiss()
Nothing in the GUI will update while there is a function that is holding the main thread, no matter what you do. So this approach minimizes time spent on the main thread. This code has not been tested, so there may be errors. But I believe this approach should accomplish what you want.
I would like to create two labels in Kivy that update their text with sensor data from temp sensors.
Temp sensors are connected to an Arduino, which prints their values to serial in the example format every two seconds or so:
A 82.4 (on line 1)
B 80.6 (on line 2)
The A/B is included in each print as an identifier that python could pick up to differentiate between the two.
The issue is importing this data into python and attaching it to labels.
Here is the existing .py:
import kivy
kivy.require('1.10.0')
from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager, Screen, FadeTransition
from kivy.app import App
from kivy.uix.label import Label
from kivy.uix.image import Image
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.stacklayout import StackLayout
from kivy.uix.button import Button
from kivy.clock import Clock
from kivy.properties import StringProperty, NumericProperty, ObjectProperty
from digitalclock import DigitalClock
from kivy.animation import Animation
import serial
import time
import opc
class IntroScreen(Screen):
pass
class ContScreen(Screen):
pass
class ScreenManagement(ScreenManager):
pass
#Disregard this, a timer is included in layout
class Timer(Label):
a = NumericProperty() # seconds
def __init__(self, root, instance, duration, bg_color, **kwargs):
super(Timer, self).__init__(**kwargs)
self.obj = instance
self.a = duration
self.root = root
self.obj.disabled = True # disable widget/button
self.obj.background_color = bg_color
self.root.add_widget(self) # add Timer/Label widget to screen, 'cont'
def animation_complete(self, animation, widget):
self.root.remove_widget(widget) # remove Timer/Label widget to screen, 'cont'
self.obj.background_color = [1, 1, 1, 1] # reset to default colour
self.obj.disabled = False # enable widget/button
def start(self):
Animation.cancel_all(self) # stop any current animations
self.anim = Animation(a=0, duration=self.a)
self.anim.bind(on_complete=self.animation_complete)
self.anim.start(self)
def on_a(self, instance, value):
self.text = str(round(value, 1))
class Status(FloatLayout):
_change = StringProperty()
_tnd = ObjectProperty(None)
def update(self, *args):
self.time = time.asctime()
self._change = str(self.time)
self._tnd.text = str(self.time)
print (self._change)
#Here is where I start referencing Serial Comms, this line is to identify where
#to *send* commands to via a separate identifier.
bone = serial.Serial('/dev/ttyACM0', 9600)
class XGApp(App):
time = StringProperty()
sensor1 = NumericProperty(0)
sensor2 = NumericProperty(0)
def update(self, *args):
self.time = str(time.asctime())
arduino = self.arduino
data = arduino.read(arduino.inWaiting())
for line in data.split('\n'):
try:
sensor, value = line.strip().split(' ')
except:
print("parse error!")
continue
if sensor == 'A':
self.sensor1 = float(value)
elif sensor == 'B':
self.sensor2 = float(value)
else:
print("unknown data! {}".format(line))
def build(self):
try:
self.arduino = serial.Serial('/dev/ttyACM0', 9600)
except Exception as e: print(e)
Clock.schedule_interval(self.update, 1)
return Builder.load_file("main.kv")
xApp = XGApp()
if __name__ == "__main__":
xApp.run()
and the .kv:
<ContScreen>:
FloatLayout
orientation: 'vertical'
padding: [10,50,10,50]
spacing: 50
Label:
id: 'TempLabel1'
text: str(app.sensor1)
color: 1,1,1,1
font_size: 80
pos_hint: {'center_x':0.2, 'center_y':0.6}
Label:
id: 'TempLabel2'
text: str(app.sensor1)
color: 1,1,1,1
font_size: 80
pos_hint: {'center_x':0.5, 'center_y':0.6}
later in the .kv:
StackLayout
orientation: "tb-rl"
spacing: 15
Button:
text: "1"
size_hint: None, .16
width: 225
on_press:
Timer(root, self, 10, [100, 0, 100, 1.75]).start()
bone.Write('j'.encode())
print("One Executed")
TempLabel1 and TempLabel2 are the two labels i'd like updated from the sensors.
It's totally possible. But you are missing a few things.
You are trying to connect to the serial port after running your app, that won't work as your app will be stopped when you arrive there. Instead, you want to do this part while your app runs. I would do the try/except to connect to arduino in app.build.
def build (self):
try:
self.arduino = serial.Serial('/dev/ttyACM0')
exept:
print("unable to connect to arduino :(")
Clock.schedule_interval(self.update, 1)
return Builder.load_file("main.kv")
then, you want to check for messages in the update method, but you don't want to block, so you only read the amount of data that is waiting in the buffer.
def update(self, *args):
arduino = self.arduino
data = arduino.read(arduino.inWaiting())
then you do your processing, i assume your data is something like:
A value
B value
in such case, you can parse it and update the corresponding variable, something like:
def update(self, *args):
arduino = self.arduino
data = arduino.read(arduino.inWaiting())
for line in data.split('\n'):
try:
sensor, value = line.strip().split(' ')
except:
print("parse error!")
continue
if sensor == 'A':
self.sensor1 = float(value)
elif sensor == 'B':
self.sensor2 = float(value)
else:
print("unknown data! {}".format(line))
would do the job, it's a bit simplistic, as it assumes you always get full lines, but it can be improved later if needed (and it seems enough for a lot of cases in my experience).
Now, we need to make sure that our labels notice the changes of values, for this, kivy uses properties, which are smarter attributes, you need to declare them on the app class.
class XGApp(App):
sensor1 = NumericProperty(0)
sensor2 = NumericProperty(0)
now, you can make your update display the value directly, through the app instance.
<ContScreen>:
FloatLayout
orientation: 'vertical'
padding: [10,50,10,50]
spacing: 50
Label:
id: 'TempLabel1'
text: str(app.sensor1)
color: 1,1,1,1
font_size: 80
pos_hint: {'center_x':0.2, 'center_y':0.6}
Label:
id: 'TempLabel2'
text: str(app.sensor2)
color: 1,1,1,1
font_size: 80
pos_hint: {'center_x':0.5, 'center_y':0.6}