After making sure queues are processed in separate threads, my kivyapp doesn’t show the embedded image icons reliably anymore. weirdly sometimes they are shown, sometimes not. What is happening here?
Before threading some processes I had everything in the main thread. No problems with showing the images, however other processes like updating the labels according to commands in the queues didn’t work reliably. Now it’s the other way around.
This is my python code (shortened, ’cause character limitations):
Builder.load_file("gui/rebuilder.kv")
Window.size = (1024, 600)
# add stringproperty to class Image
class Image(Widget):
#initialize StringProperty
status : StringProperty = StringProperty(None)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.bind(status=self.on_status_change)
def on_status_change(self, instance, value:str):
# check if status changed
return value
# main thread for App
class MyGridLayout(Widget):
# global variables for pathing-thread and cubedetector-thread
pathing: Pathing | None = None
pathing_thread : Process = None
cube_detection_thread: Process = None
# initialize more variables, threads, etc.
def __init__(self, **kwargs):
super().__init__(**kwargs)
# queues
self.queues: SystemQueues = SystemQueues()
self.bilderkennung_to_gui = self.queues.gui_bilderkennung_rx
self.gui_to_bilderkennung = self.queues.gui_bilderkennung_tx
self.pathing_to_gui = self.queues.gui_pathing_rx
self.gui_to_pathing = self.queues.gui_pathing_tx
# variables
self.time_started = False
self.time_seconds = 0
self.progress_started = False
self.energy_started = False
self.homing = False
self.threads_running = True
self.i = 1
# create trigger for emergencies
self.emergency_tracker = False
self.emergency_count = 0
# keep track of scheduled events
self.scheduled: deque = deque()
# check for emergencies
self.schedule_interval(self.emergency_check,1)
# start queues
self.__internal_queue: Queue = Queue()
queue_handler_thread: threading.Thread =
threading.Thread(target=self.queue_handler, daemon=True)
queue_handler_thread.start()
queue_pathing_handler_thread: threading.Thread =
threading.Thread(target=self.__gui_messenge_receiver, daemon=True)
queue_pathing_handler_thread.start()
queue_bilderkennung_handler_thread: threading.Thread =
threading.Thread(target=self.__bilderkennung_messenge_receiver, daemon=True)
queue_bilderkennung_handler_thread.start()
"""# check for connectivity
Clock.schedule_interval(self.wlan_check, 5)"""
# initiate threads
self.pathing: Pathing
self.init_all()
def init_all(self) -> None:
MyGridLayout.pathing = PathingFactory.setup_pathing(self.queues)
# initialize threads
MyGridLayout.pathing_thread = Process(target=self.pathing.run)
MyGridLayout.cube_detection_thread = Process(target=init_cube_detection, args=(self.queues, ))
MyGridLayout.pathing_thread.start()
MyGridLayout.cube_detection_thread.start()
# multiple threads for messages between pathing, bildanalyse and gui --> avoid race condition
def __gui_messenge_receiver(self) -> None:
while True:
try:
get_pathing = self.pathing_to_gui.get()
Logger.info(f"get pathing {get_pathing}")
except Exception:
get_pathing = None
self.__internal_queue.put(get_pathing)
def __bilderkennung_messenge_receiver(self) -> None:
while True:
try:
get_bildanalyse = self.bilderkennung_to_gui.get()
Logger.info(f"get bildanalyse {get_bildanalyse}")
except Exception:
get_bildanalyse = None
self.__internal_queue.put(get_bildanalyse)
def queue_handler(self):
Logger.debug("Gui Queuehandler started")
while True:
# check for object in queue bildanalyse
get_queue = None
get_queue = self.__internal_queue.get()
Logger.debug("Reveived from int queue")
Logger.info(f"get_queue = {get_queue}")
"""# check for activity in pathing queue
prev_get_pathing = None
if get_pathing != prev_get_pathing:
Logger.info(get_pathing)"""
# check for object in queue bildanalyse
"""# check for activity in bildanalyse queue
prev_get_bildanalyse = None
if get_bildanalyse != prev_get_bildanalyse:
Logger.info(get_bildanalyse)"""
try:
if isinstance(get_queue, Setup):
self.home_update(get_queue)
elif isinstance(get_queue, MeasureResults):
self.energy_update(get_queue)
elif isinstance(get_queue, Build):
self.start_update_pathing(get_queue)
elif isinstance(get_queue, Analyse):
self.start_update_bildanalyse(get_queue)
elif isinstance(get_queue, Reset):
self.stop_processes_update(get_queue)
elif isinstance(get_queue, Emergency):
self.emergency(get_queue)
except Exception as e:
print(f"Fehler aufgetreten {e}")
# processes while homing 3D Rebuilder
def home_rebuilder(self):
command = Setup("home")
self.gui_to_pathing.put(command)
# update progress label
self.update_progress_label("Gerät wird kalibriert...")
self.schedule_event(self.clear_progress_bar, 0)
def home_update(self, get_pathing: Setup):
# progress update homing
if get_pathing.ans == "ack":
self.update_progress_label("Gerät wird kalibriert...")
zeiten = [0.1, 1.5, 2.2, 3.9, 5.2, 6.7, 8, 9.4, 10.7]
for zeit in zeiten:
self.schedule_event(self.update_progress_bar, zeit)
elif get_pathing.ans == "done":
self.ids.progress_label.text = "Gerät ist betriebsbereit."
self.schedule_event(self.update_progress_bar, 0)
#Start-Button aktivieren
self.ids.start.disabled = False
self.ids.reset.disabled = False
#Anzeigeelemente aktivieren
self.ids.timer.status = "on"
self.ids.strom.status = "on"
# starting 3D-Rebuilding process
def start(self):
#Clear Progress_bar
self.schedule_event(self.clear_progress_bar, 0)
#Startbefehl an Pathing für Strommessung
command = Control("start")
self.gui_to_pathing.put(command)
self.gui_to_bilderkennung.put(command)
# Update progress label
if self.progress_started == False:
self.progress_started = True
# update label, progressbar
self.update_progress_label("Bildanalyse initialisieren")
self.schedule_event(self.update_progress_bar, 0)
# start timer
self.time_started = True
self.schedule_interval(self.zeit_update, 0)
# start energy update
self.energy_started = True
self.schedule_interval(self.energy_ask, 1.5)
# Deactivate start, reset
self.ids.start.disabled = True
self.ids.reset.disabled = True
self.ids.stop.disabled = False
def start_update_bildanalyse(self, get_bildanalyse: Analyse):
if self.progress_started:
if get_bildanalyse:
#updates = ["Quadrant erkannt", "Bilderkennung..", "Bilderkennung..", "Validierung", "Bilderkennung abgeschlossen"]
if get_bildanalyse.quadrant == "quadrant":
self.update_progress_label("Quadrant erkannt")
self.schedule_event(self.update_progress_bar, 0)
elif get_bildanalyse.first_side == "first_side":
self.update_progress_label("Bilderkennung..")
self.schedule_event(self.update_progress_bar, 0)
elif get_bildanalyse.second_side == "second_side":
self.schedule_event(self.update_progress_bar, 0)
elif get_bildanalyse.validate == "validate":
self.update_progress_label("Validierung")
self.schedule_event(self.update_progress_bar, 0)
elif get_bildanalyse.client_closed == "client_closed":
self.schedule_event(self.update_progress_bar, 0)
def start_update_pathing(self, get_pathing: Build):
if self.progress_started:
if get_pathing:
if get_pathing.ready == "ready":
self.update_progress_label("Pfadberechnung")
self.schedule_event(self.update_progress_bar, 0)
elif get_pathing.first_side == "first_side":
self.update_progress_label("Nachbau..")
self.schedule_event(self.update_progress_bar, 0)
elif get_pathing.second_side == "second_side":
self.schedule_event(self.update_progress_bar, 0)
elif get_pathing.done == "done":
# update label
self.schedule_event(self.update_progress_bar, 0)
self.update_progress_label("3D rebuilding abgeschlossen.")
# play sound
radio_jingle: SoundEffect = SoundEffect("Common/Audio/radio-jingle.wav")
radio_jingle.play()
# make sure progressbar is fully updated
rest = 11-self.i
Logger.info(f"rest = {rest}")
for r in range(rest):
Logger.info(f"self.i = {self.i}")
self.schedule_event(self.update_progress_bar, 0.1*r)
# stop processes
self.stop_label_update()
self.stop_all_processes()
# stopping 3D - rebuilding --> this process is reached through: end of normal process, press stop button, after emergencies
def stop_label_update(self):
# stop timer, progress and energy update
self.progress_started = False
self.time_started = False
self.energy_started = False
def stop_all_processes(self):
# send delete message to pathing, bildanalyse --> initiate stop/reset of processes
command = Reset("delete")
self.gui_to_bilderkennung.put(command)
self.gui_to_pathing.put(command)
# set thread status to false
self.threads_running = False
# deactivate stop button
self.ids.stop.disabled = True
def stop_processes_update(self, get_pathing: Reset):
#Answer from pathing
if get_pathing.ans == "ack":
# activate reset button
self.ids.reset.disabled = False
self.update_progress_label("Vor dem Reset Würfel entfernen.")
# unschedule Clock-events
self.cancel_scheduled()
# stop button
def stop_button(self):
# stop updates on gui
self.stop_label_update()
# initiate stop of all processes
self.stop_all_processes()
# update label
self.update_progress_label("Prozesse werden angehalten.")
# process for reset
def reset(self, *args):
# initiate new threads
if not self.threads_running:
MyGridLayout.pathing = None
MyGridLayout.pathing_thread: Process = None
MyGridLayout.cube_detection_thread: Process = None
bilderkennung_to_pathing = self.queues.bilderkennung_pathing_rx
pathing_to_bilderkennung = self.queues.bilderkennung_pathing_tx
self.empty_queue(bilderkennung_to_pathing)
self.empty_queue(pathing_to_bilderkennung)
self.empty_queue(self.bilderkennung_to_gui)
self.empty_queue(self.gui_to_bilderkennung)
self.empty_queue(self.pathing_to_gui)
self.empty_queue(self.gui_to_pathing)
self.init_all()
self.threads_running = True
Logger.info(f"Threads {self.threads_running}")
# update label
self.ids.progress_label.text = "Gerät zurücksetzen"
self.schedule_event(self.clear_progress_bar, 0)
Logger.info(f"scheduled stack {self.scheduled}")
# reset counter, labels, timer
self.i = 1
self.time_seconds = 0
self.ids.energy.text = "0.0 Ws"
self.ids.strom.status = "off"
self.ids.timer.status = "off"
# deactivate buttons
self.ids.reset.disabled = True
self.ids.start.disabled = True
self.home_rebuilder()
# empty queues for reset
def empty_queue(self, queue: Queue):
while not queue.empty():
queue.get()
"""def wlan_check(self, dt):
connected = ConnectivityTest.test_internet_connectivity()
if connected:
image_path = "gui/images/wlan_on.png"
else:
image_path = "gui/images/wlan_off.png"
self.ids.wlan.source = image_path"""
#Build App as usual, left out 'cause character limitations
This is my kv-file:
#:import Factory kivy.factory.Factory
<Label>
font_size: 28
color: (.75,.75,.75,.7)
<Button>
background_color: 0,0,0,0
font_size: 24
color: (0,.6,.9,.8) if self.state == "normal" else (0,.6,.9,1)
text_size: self.width, self.height
halign: "center"
valign: "middle"
<RoundedButton@Button>
#padding: 0,0,90,0
canvas.before:
Color:
rgba: (0, .6, .9, .8) if self.disabled == False else (.2,.2,.2,1)
RoundedRectangle:
size: self.size
pos: self.pos
radius: [5,]
Color:
rgba: (.05, .05, .05, 1)
RoundedRectangle:
size: self.width-4, self.height-4
pos: self.x+2, self.y+2
radius: [5,]
Color:
rgba: (0, 0, 0, 0) if self.state == "normal" else (0,.6,.9,.6)
RoundedRectangle:
size: self.size
pos: self.pos
radius: [5,]
<Cube>
status: "normal"
canvas:
Color:
rgba: (.14, .14, .14, 1) if self.status == "normal" else (0,.5, .8, .8)
RoundedRectangle:
size: self.size
pos: self.pos
radius: [5, ]
#Popups definieren
<StartupPopup@Popup>
id: startup_popup
grid_layout_instance: None
auto_dismiss: False
#size_hint: 0.9,0.9
pos_hint: {"center_x":0.5, "center_y":0.5}
background_color: (0.05, .05, .05, 1)
title: ""
separator_height: 0
GridLayout:
cols: 1
size: root.width, root.height
padding: 130, 0, 130, 80
BoxLayout:
orientation: "vertical"
padding: 0, -150, 0, 0
Image:
source: "gui/images/cube_3d_b.png"
Label:
font_size: 24
text_size: root.width, None
size: self.texture_size
#padding: 10, 10, 10, 70
text: "Willkommen bei 3DRebuilder."
halign: "center"
valign: "center"
GridLayout:
cols: 1
row_default_height: 100
row_force_default: True
padding: 130, 0, 130, 0
RoundedButton:
text: "Los gehts!"
halign: "center"
valign: "center"
#padding: 0, 0, 0, 0
on_release:
root.dismiss()
app.root.home_rebuilder()
<EmergencyPopup@Popup>
id: emergency_popup
grid_layout_instance: None
auto_dismiss: False
size_hint: 0.8,0.8
pos_hint: {"x":0.1, "top":0.9}
#background_color: (0.05, .05, .05, 1)
title: "Notaus"
GridLayout:
cols: 1
size: root.width, root.height
padding: 50, 10, 80, 10
Label:
id: popup_label
text_size: self.size
#size: self.texture_size
bg_color: (1, .2, 0, 1)
#padding: 10
text: "Der Notaus wurde gedrückt. n 3DRebuilder überprüfen und verkeilte Teile entfernen."
halign: "center"
valign: "center"
GridLayout:
cols: 1
row_default_height: 100
row_force_default: True
padding: 80, 0, 80, 0
RoundedButton:
text: "Problem behoben"
halign: "center"
padding: 0,0,0,0
on_release:
#root.dismiss()
app.root.close_em_popup()
#Layout GUI
<MyGridLayout>
#Contains all the other rows
GridLayout
cols:1
size: root.width, root.height
padding: 30, 50, 30, 0 #Padding Hauptframe
BoxLayout:
orientation: "vertical"
#Logo and Wifi-Elements
GridLayout:
id: row_4
cols: 3
GridLayout:
cols: 1
Image:
source: "gui/images/logo_b.png"
size_hint_y: None # Tells the layout to ignore the size_hint in y dir
height: dp(70) # The fixed height you want
GridLayout:
cols: 1
GridLayout:
cols: 2
BoxLayout:
orientation: 'horizontal'
# Align the content to the right within the BoxLayout
padding: (160, 35, 80, 0) # Padding to the left to keep space
Image:
id: wlan
source: "gui/images/wlan_on.png"
size_hint_y: None # Tells the layout to ignore the size_hint in y dir
height: dp(35) # The fixed height you want
pos_hint: {"center_x":1, "center_y":1}
#Image:
#source: "gui/images/empfang_3.png"
#size_hint_y: None # Tells the layout to ignore the size_hint in y dir
#height: dp(25) # The fixed height you want
#pos_hint: {"center_x":.7, "center_y":1}
#Row for Buttons
GridLayout:
id: row_1
cols: 3
padding: 30, -40, 30, 0
spacing: 5
row_default_height: 100
row_force_default: True
#Start Button
RoundedButton:
id: start
name: start
text: "Start"
disabled: True
on_release:
root.start()
Image:
source: "gui/images/start.png" if self.disabled == False else "gui/images/start_off.png"
center_x: self.parent.center_x-55
center_y: self.parent.center_y
size_hint_y: None
height: dp(40)
#Stop Button
RoundedButton:
id: stop
text: "Stop"
disabled: True
on_release:
root.stop_button()
Image:
source: "gui/images/stop.png" if self.disabled == False else "gui/images/stop_off.png"
center_x: self.parent.center_x-55
center_y: self.parent.center_y
size_hint_y: None
height: dp(40)
#Reset Button
RoundedButton:
id: reset
text: "Reset"
disabled: True
on_release:
root.reset()
Image:
source: "gui/images/reset.png" if self.disabled == False else "gui/images/reset_off.png"
center_x: self.parent.center_x-55
center_y: self.parent.center_y
size_hint_y: None
height: dp(40)
#Gridlayout for visual feedback elements
GridLayout:
id: row_3
cols: 2
padding: 30, 0
#Progress feedback
GridLayout:
cols: 1
rows: 2
spacing: 10
row_default_height: 100
row_force_default: True
BoxLayout:
padding: 0,30,0,0
Label:
id: progress_label
text_size: root.width, None
size_hint: 1, 1
size: self.texture_size
text: "Gerät vorbereiten"
halign: "center"
pos_hint: {"y":.75}
#Progress-Cubes
BoxLayout:
id: cubecontainer
orientation: "horizontal"
size_hint: 1, 1
padding: 60, -10, 0, 0
spacing: 5
Cube:
id: p1
size_hint: None, None
size: 30,30
pos_hint: {"center_x": 1, "center_y": 1}
Cube:
id: p2
size_hint: None, None
size: 30,30
pos_hint: {"center_x": 1, "center_y": 1}
Cube:
id: p3
size_hint: None, None
size: 30,30
pos_hint: {"center_x": 1, "center_y": 1}
Cube:
id: p4
size_hint: None, None
size: 30,30
pos_hint: {"center_x": 1, "center_y": 1}
Cube:
id: p5
size_hint: None, None
size: 30,30
pos_hint: {"center_x": 1, "center_y": 1}
Cube:
id: p6
size_hint: None, None
size: 30,30
pos_hint: {"center_x": 1, "center_y": 1}
Cube:
id: p7
size_hint: None, None
size: 30,30
pos_hint: {"center_x": 1, "center_y": 1}
Cube:
id: p8
size_hint: None, None
size: 30,30
pos_hint: {"center_x": 1, "center_y": 1}
Cube:
id: p9
size_hint: None, None
size: 30,30
pos_hint: {"center_x": 1, "center_y": 1}
Cube:
id: p10
size_hint: None, None
size: 30,30
pos_hint: {"center_x": 1, "center_y": 1}
#Timer and Energy-Update
GridLayout:
cols: 1
rows: 2
spacing:10
padding: 0, -60, 40, 0
row_default_height: 100
row_force_default: True
GridLayout:
cols: 1
BoxLayout:
orientation: "vertical"
size_hint: (1, None)
height: dp(30)
padding: (0,0,185,-37)
Image:
id: timer
source: "gui/images/timer.png" if self.status == "on" else "gui/images/timer_off.png"
status: "off"
size_hint_y: None # Tells the layout to ignore the size_hint in y dir
height: dp(30) # The fixed height you want
BoxLayout:
orientation: "vertical"
size_hint: (1, 1)
padding: (0,0,0,30)
Label:
id: zeit
text: "00:00:00"
GridLayout:
cols: 1
BoxLayout:
orientation: "vertical"
size_hint: (1, None)
height: dp(30)
padding: (0,0,180,-39)
Image:
id: strom
status: "off"
source: "gui/images/energy.png" if self.status == "on" else "gui/images/energy_off.png"
size_hint_y: None # Tells the layout to ignore the size_hint in y dir
height: dp(30) # The fixed height you want
BoxLayout:
orientation: "vertical"
size_hint: (1, 1)
padding: (0,0,0,25)
Label:
id: energy
text: "0.0 Ws"
RelativeLayout:
Label:
font_size: 20
text: "Gruppe 30"
pos_hint: {"center_x": .15, "center_y": .4}