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}
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 am trying to update the position of a widget via a NumericProperty. When the function has been called the NumericProperty wont update. Updating inside of the function does not update the pos of my widget; updating outside yields an error. What can I do?
This code has been simplified
main.py
from kivy.app import App
from kivy.clock import mainthread
from kivy.properties import NumericProperty, BooleanProperty
class Game(App):
X_value = 0
X_value = 0
GPS_X = NumericProperty(0)
GPS_Y = NumericProperty(0)
#mainthread
def on_location(self, *args, **kwargs):
self.my_lat = kwargs['lat']
self.my_lon = kwargs['lon']
self.pos_updater()
return self.my_lat, self.my_lon
def pos_updater(self):
scaler = 100
self.Y_value = scaler * 111320.0
self.X_value = scaler * 40075000.0 * math.cos(self.my_lat)/360.0
print("Delta:",self.Y_value, self.X_value)
GPS_X += X_value + 100
GPS_Y += Y_value
def build(self):
pass
Game().run()
main.kv
<Game>
BoxLayout:
orientation: 'vertical'
FloatLayout:
size_hint: 1, 3
Button:
text: "higher"
pos: (root.GPS_X, root.GPS_Y + root.height/2)
on_release:
root.on_location()
Label:
id: time_id
text: "test"
Error Message:
TypeError: unsupported operand type(s) for +=: 'kivy.properties.NumericProperty' and 'int'
I am assuming ur are using a mapview:
in kivy file :
MapView:
id: main_map
size: root.width, root.height
MapMarkerPopup:
source: 'pin.png'
id: main_map_me
Now in ur main.py try this:
def on_location(self, *args, **kwargs):
self.my_lat = kwargs['lat']
self.my_lon = kwargs['lon']
self.ids.main_map_me.lat = self.my_lat
self.ids.main_map_me.lon = self.my_lon
its EASIER by id, use properties only when u cant use ids.(my opinion)
lets say ur Button has an id(btn)
in ur kivy file try this:
Button:
id: btn
in ur main.py try this:
self.ids.btn.pos[0] = self.width*0.5 # or whatever u need.....
self.ids.btn.pos[1] = self.height* #..... whatever u need
And check the type of lat and lon if they are not strings cuz u are assuming that they are NumericProperty i think they are StringProperty if so just convert them and work with ur original code stringproperty = float(my_Lat)
And at last i dont know what u are trying to do with the scaler exactly but u must know that the screen position has nothing to do the position thats on the mapview.
I have this code below. I've set id by python code, but I couldn't access.
def abrirReceita(self,instance):
instance.text = str(instance.ids)
I'd like to change the text with the number of the ID when I press.
Exemple: if I input the first button, change the text for '1', which is the ID I've passed.
from kivymd.app import MDApp
from kivymd.uix.boxlayout import BoxLayout
from kivymd.uix.floatlayout import FloatLayout
from kivymd.uix.list import TwoLineListItem
from kivymd.uix.textfield import MDTextField
from kivy.lang import Builder
import os
from kivy.core.window import Window
import sqlite3
KV = '''
ScreenManager:
Screen:
name: 'telaSelecionada'
BoxLayout:
orientation: 'vertical'
MDToolbar:
id: tb
title: 'Drinks'
md_bg_color: 0, 0, 0, 1
TelaSelecionada:
id: telaselecionada
<TelaSelecionada>:
ScrollView:
MDList:
id: mostraReceita
padding: '20dp'
'''
Window.softinput_mode = "below_target"
class TelaSelecionada(FloatLayout):
pass
class Aplicativo(MDApp):
def build(self):
return Builder.load_string(KV)
def on_start(self):
i = 1
for x in range(5):
self.textLine = TwoLineListItem(text = 'change text', secondary_text = 'change text')
self.root.ids.telaselecionada.ids.mostraReceita.add_widget(self.textLine)
self.root.ids.telaselecionada.ids.mostraReceita.ids[i] = self.textLine
self.textLine.bind(on_release = self.abrirReceita)
i += 1
def abrirReceita(self,instance):
instance.text = str(instance.ids)
Aplicativo().run()
How do I access the IDs from python code?
I'd like to change the text with the number of the ID when I press.
Just pass the required args through method abrirReceita using functools.partial as,
def on_start(self):
...
self.textLine.bind(on_release = partial(self.abrirReceita, i))
i += 1
# Then in `abrirReceita` :
def abrirReceita(self, id_index, instance):
instance.text = str(id_index)
Note:
The ids are not used here at all (actually you don't need that here for this purpose) !
Update:
As, the Widget.id has been deprecated since version 2.0.0 you should use it only in kvlang not in python.
But that doesn't keep you from creating it as a dynamically added attribute. Thus you can do something like this (I modified the on_start method a little):
def on_start(self):
mostraReceita = self.root.ids.telaselecionada.ids.mostraReceita
for i, _ in enumerate(range(5), start=1):
# Or just,
# for i in range(1, 5+1):
self.textLine = TwoLineListItem(
text = 'change text',
secondary_text = 'change text',
)
self.textLine.index = i
# Or even,
# self.textLine.id = i
self.textLine.bind(on_release = self.abrirReceita)
mostraReceita.add_widget(self.textLine)
def abrirReceita(self,instance):
instance.text = str(instance.index) # or, str(instance.id)
I'm trying to build a Kivy application that has 2 screens which are re-used over and over again with different text.
So I go from a FirstScreen with a Label that says "First1" to a SecondScreen with a Label that says "Second1", and then back to the FirstScreen but this time with the Label "First2", then SecondScreen and "Second2", and so on and so forth.
The code for this is pretty straightforward, but there seems to be a problem in updating the Label text without a designated update button. For some reason, my Python code manages to update the text, but it isn't updated in my .kv file. So for instance, my print statements will tell me that the Label text is "First2", but Kivy displays "First1" for me. I've illustrated this in the Screenshot below:
By adding a Button that updates the text on press, everything is updated, synced up and works, but I'd really like it to work without the extra user input. Does anybody know how I can go about this? I've scoured the docs and stackoverflow questions left and right but can't seem to find the answer to my seemingly simple problem.
Here's the code:
from kivy.app import App
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.properties import StringProperty, ObjectProperty
from kivy.lang import Builder
S_ID = 1 # global screen ID. I'm using this to detect what text to use.
class FirstScreen(Screen):
text = StringProperty("")
lbl = ObjectProperty(None)
def __init__(self, **kwargs):
super(FirstScreen, self).__init__(**kwargs)
global S_ID
print("\nS_ID is ", S_ID)
self.update()
def update(self):
print("FIRST - UPDATE")
if S_ID == 1:
print("FIRST1")
self.text = "FIRST1"
elif S_ID == 2:
print("FIRST2")
self.text = "FIRST2"
print("self.lbl.text", self.lbl.text)
else:
print("FIRST ELSE")
self.text = "FIRST ELSE"
def pressed(self):
sm.current = "second"
class SecondScreen(Screen):
text = StringProperty("")
def __init__(self, **kwargs):
super(SecondScreen, self).__init__(**kwargs)
self.update()
def update(self):
print("SECOND - UPDATE")
if S_ID == 1:
print("SECOND1")
self.text = "SECOND1"
elif S_ID == 2:
print("SECOND2")
self.text = "SECOND2"
else:
print("SECOND ELSE")
self.text = "SECOND ELSE"
def pressed(self):
global S_ID
S_ID += 1
FirstScreen.update(FirstScreen())
sm.current = "first"
sm = ScreenManager()
kv = Builder.load_file("test.kv")
sm.add_widget(FirstScreen(name='first'))
sm.add_widget(SecondScreen(name='second'))
sm.current = "first"
class MyApp(App):
def build(self):
return sm
if __name__ == '__main__':
MyApp().run()
and here's the .kv file:
<FirstScreen>:
name: "first"
lbl: lbl:
GridLayout:
cols:2
Label:
id: lbl
text: root.text
Button:
text: "next"
on_press: root.pressed()
<SecondScreen>:
name: "second"
GridLayout:
cols:2
Label:
text: root.text
Button:
text: "next"
on_press:
root.pressed()
The problem is your statement:
FirstScreen.update(FirstScreen())
This statement is creating a new instance of FirstScreen and updating that instance. Unfortunately, that instance is not the one shown in your GUI. You can correct that by replacing the above statement with:
first_screen = self.manager.get_screen('first')
first_screen.update()
This code gets the instance of FirstScreen from the ScreenManager and calls update() on that instance.
How do you update a progress bar that is displayed in Kivy. In the following example I get AttributeError: 'super' object has no attribute '__getattr__'. The issue is in the following line
self.ids.progress.value = value
I can see why since the progressBar widget is in <LoadingPopup> and not <MainScreen> but after trying several different things I can't reference the progressBar widget in <LoadingPopup> from the do_update method.
Thanks in advance
import threading
from functools import partial
from kivy.lang import Builder
from kivy.uix.floatlayout import FloatLayout
from kivy.properties import StringProperty
from kivy.uix.spinner import Spinner
from kivy.app import App
from kivy.clock import Clock
from kivy.uix.popup import Popup
Builder.load_string('''
<LoadingPopup>:
title: "Popup"
size_hint: None, None
size: 400, 400
auto_dismiss: False
BoxLayout:
orientation: "vertical"
ProgressBar:
id: progress
size_hint: (1.0, 0.06)
<MainScreen>:
BoxLayout:
orientation: 'vertical'
Spinner:
id: first
text: ' First Number'
values: ['1','2','3','4','5','6','7','8','9']
Spinner:
id: second
text: ' Second Number'
values: ['1','2','3','4','5','6','7','8','9']
Label:
id: result
text: ' Result'
color: 0,0,0,1
Button:
id: res
on_press: root.doit_in_thread(first.text,second.text)
text: 'Multiply'
''')
class LoadingPopup(Popup):
def __init__(self, obj, **kwargs):
super(LoadingPopup, self).__init__(**kwargs)
class MainScreen(FloatLayout):
changet = StringProperty()
def __init__(self, **kwargs):
super(MainScreen, self).__init__(**kwargs)
def doit_in_thread(self, fir, sec):
popup = LoadingPopup(self)
popup.open()
threading.Thread(target=partial(self.onMul, fir, sec, popup)).start()
def do_update(self, value, text, *args):
self.ids.progress.value = value
self.ids.result.text = text
def onMul(self,fir,sec, popup):
a = (int(fir)*int(sec))
print(a)
b = 0
old_value = 0
endRange = 1000000
for i in range(endRange):
progress = int(((i+1)*100)/endRange)
if progress != old_value and progress % 5 == 0:
text = str(b*(int(fir)*int(sec)))
Clock.schedule_once(partial(self.do_update, progress, text))
old_value = progress
b+=1
popup.dismiss()
class TestApp(App):
def build(self):
return MainScreen()
if __name__ == "__main__":
TestApp().run()
One way to do it, is to keep a reference of the popup so you can address it later:
def __init__(self, **kwargs):
super(MainScreen, self).__init__(**kwargs)
self.popup = LoadingPopup(self)
def doit_in_thread(self, fir, sec):
self.popup.open()
threading.Thread(target=partial(self.onMul, fir, sec, self.popup)).start()
def do_update(self, value, text, *args):
self.popup.ids.progress.value = value
self.ids.result.text = text
Python Code
Replace all references of popup with self.popup so that you can access it in the methods inside class MainScreen.
Replace self.ids.progress.value with self.popup.ids.progress.value
Since a = (int(fir)*int(sec), replace str(b*(int(fir)*int(sec))) with str(b * a)
Don't need to pass popup to onMul method, access it using self.popup
Snippet
class MainScreen(FloatLayout):
changet = StringProperty()
def doit_in_thread(self, fir, sec):
self.popup = LoadingPopup(self)
self.popup.open()
threading.Thread(target=partial(self.onMul, fir, sec)).start()
def do_update(self, value, text, *args):
self.popup.ids.progress.value = value
self.ids.result.text = text
def onMul(self, fir, sec):
a = (int(fir)*int(sec))
print(a)
b = 0
old_value = 0
endRange = 1000000
for i in range(endRange):
progress = int(((i+1)*100)/endRange)
if progress != old_value and progress % 5 == 0:
text = str(b*(int(fir)*int(sec)))
# text = str(b * a)
print("\ttext=", text)
Clock.schedule_once(partial(self.do_update, progress, text))
old_value = progress
b+=1
self.popup.dismiss()
kv file
The result is not visible because the default background colour of Label widget is black, and the text colour for result is also set to black, color: 0,0,0,1. Therefore, remove color: 0,0,0,1.
Snippet
Label:
id: result
text: ' Result'
Output