Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Update labels in a separate worker (Process instance)

I do have several screens. One of them (DataScreen) contains 8 labels which should show the current sensor values. Sensors are read by a separate process (which is started from the MainScreen). The process itself is an instance of multiprocessing.Process.

I can get a reference to the labels by sensor_labels = self.manager.get_screen('data').l

However, I cannot figure out how to change them within the subprocess. I can change them from any function which is not a separate process, simply by doing something like:

for item in sensor_labels:
    item.text = 'Update'

Unfortunately, it seems to be more difficult to pass the reference of the sensor_labels to the worker. If I pass them as argument both processes (kivy and the worker) seem to share the same object (the id is the same). However, if I change label.text = 'New Text' nothing changes in Kivy.

Why is the id of both objects the same, but the text is not changed ? And how can I share a Kivy label object with another process ?

Here is my working minimal example

#! /usr/bin/env python
""" Reading sensor data
"""
from kivy.config import Config
Config.set('kivy', 'keyboard_mode', 'multi')
from kivy.app import App
from kivy.lang import Builder
from kivy.properties import StringProperty, ObjectProperty, NumericProperty
from kivy.uix.label import Label
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.stacklayout import StackLayout
from multiprocessing import Process, Queue, Array
# all other modules
import time
import numpy as np
from multiprocessing import Lock
class MainScreen(Screen):

    def __init__(self, **kwargs):
        super(MainScreen, self).__init__(**kwargs)
        self.n_probes = 8

    @staticmethod
    def read_sensors(qu_rx, sensor_labels, lock):
        while True:
            if not qu_rx.empty():
                message = qu_rx.get()
                if message == 'STOP':
                    print('Worker: received poison pill')
                    break

            data = np.random.random()
            print('ID of labels in worker: {}'.format(id(sensor_labels)))

            print('Text of labels in worker:')
            lock.acquire()
            for label in sensor_labels:
                label.text = '{0:2f}'.format(data)
                print(label.text)
            lock.release()
            time.sleep(5)

    def run_worker(self, *args, **kwargs):
        self.qu_tx_worker = Queue()
        lock = Lock()
        # this is a reference to the labels in the DataScreen class
        self.sensor_labels = self.manager.get_screen('data').l
        self.worker = Process(target=self.read_sensors,
                              args=(self.qu_tx_worker, self.sensor_labels, lock))
        self.worker.daemon = True

        self.worker.start()

    def stop_worker(self, *args, **kwargs):
        self.qu_tx_worker.put('STOP')
        print('Send poison pill')
        self.worker.join()
        print('All worker dead')

        print('ID of labels in Kivy: {}'.format(id(self.sensor_labels)))
        print('Label text in Kivy:')
        for label in self.sensor_labels:
            print(label.text)


class DataScreen(Screen):

    def __init__(self, **kwargs):
        layout = StackLayout()
        super(DataScreen, self).__init__(**kwargs)
        self.n_probes = 8
        self.label_text = []
        for i in range(self.n_probes):
            self.label_text.append(StringProperty())
            self.label_text[i] = str(i)
        self.l = []
        for i in range(self.n_probes):
            self.l.append(Label(id='l_{}'.format(i),
                          text='Start {}'.format(i),
                          font_size='60sp',
                          height=20,
                          width=20,
                          size_hint=(0.5, 0.2)))
            self.ids.stack.add_widget(self.l[i])

    def change_text(self):
            for item in self.l:
                item.text = 'Update'


Builder.load_file('phapp.kv')

class MyApp(App):
    """
    The settings App is the main app of the pHBot application.
    It is initiated by kivy and contains the functions defining the main interface.
    """

    def build(self):
        """
        This function initializes the app interface and has to be called "build(self)".
        It returns the user interface defined by the Builder.
        """

        sm = ScreenManager()
        sm.add_widget(MainScreen())
        sm.add_widget(DataScreen())
        # returns the user interface defined by the Builder
        return sm

if __name__ == '__main__':
    MyApp().run()

And the .kv file:

<MainScreen>:
    name: 'main'
    BoxLayout:
        orientation: 'vertical'
        Button:
            text: 'Start Application'
            font_size: 40
            on_release: root.run_worker()
        Button:
            text: 'Stop Application'
            font_size: 40
            on_release: root.stop_worker()
        Button:
            text: 'Go to data'
            font_size: 40
            on_release: app.root.current = 'data'
        Button:
            text: 'Exit'
            font_size: 40
            on_release: app.stop()

<DataScreen>:
    name: 'data'
    StackLayout:
        id: stack
        orientation: 'lr-tb'
    BoxLayout:
        Button:
            size_hint: (0.5, 0.1)
            text: 'Update'
            font_size: 30
            on_release: root.change_text()
        Button:
            size_hint: (0.5, 0.1)
            text: 'Back to main menu'
            font_size: 30
            on_release: app.root.current = 'main'
like image 442
Moritz Avatar asked Nov 13 '16 22:11

Moritz


1 Answers

It looks like you might misunderstand how multiprocessing works.

When you start a new Process with the multiprocessing library it creates a new process and pickles all the code needed to run the target function. Any updates you make to the labels passed are happening in the worker process and will NOT reflect in the UI process.

To get around this you have to use one of these methods to exchange data between the worker and UI processes: https://docs.python.org/2/library/multiprocessing.html#exchanging-objects-between-processes. Since you already have a queue you can do something like this:

Put your read_sensors into worker.py passing a tx and rx Queue where tx is used to send to the UI and rx is used to read from the UI.

#! /usr/bin/env python
""" Reading sensor data
"""
import time
import numpy as np

def read_sensors(rx,tx, n):
    while True:
        if not rx.empty():
            message = rx.get()
            if message == 'STOP':
                print('Worker: received poison pill')
                break

        #: Sensor value for each label
        data = [np.random.random() for i in range(n)]

        #: Formatted data
        new_labels = ['{0:2f}'.format(x) for x in data]
        print('Text of labels in worker: {}'.format(new_labels))
        #lock.acquire() # Queue is already safe, no need to lock

        #: Put the formatted label in the tx queue
        tx.put(new_labels)

        # lock.release() # Queue is already safe, no need to unlock
        time.sleep(5)

Then in your app use the Clock to call an update handler to check the tx Queue for updates periodically. When quitting, the UI can tell the worker to stop by putting a message in the rx queue.

#! /usr/bin/env python
""" Reading sensor data
"""
from kivy.config import Config
from kivy.clock import Clock
Config.set('kivy', 'keyboard_mode', 'multi')
from kivy.app import App
from kivy.lang import Builder
from kivy.properties import StringProperty, ObjectProperty, NumericProperty
from kivy.uix.label import Label
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.stacklayout import StackLayout
from multiprocessing import Process, Queue

#: Separate worker file so a separate app is not opened
import worker

class MainScreen(Screen):

    def __init__(self, **kwargs):
        super(MainScreen, self).__init__(**kwargs)
        self.n_probes = 8

        #: Hold the update event
        self._event = None

    def read_worker(self,dt):
        """ Read the data from the worker process queue"""
        #: Get the data from the worker (if given) without blocking
        if self.tx.empty():
            return # No data, try again later

        #: The worker put data in the queue, update the labels
        new_labels = self.tx.get()
        for label,text in zip(self.sensor_labels,new_labels):
            label.text = text

    def run_worker(self, *args, **kwargs):
        self.rx = Queue() #: Queue to send data to worker process 
        self.tx = Queue() #: Queue to recv from worker process
        self.sensor_labels = self.manager.get_screen('data').l
        self.worker = Process(target=worker.read_sensors,
                              args=(self.rx,self.tx,self.n_probes))
        self.worker.daemon = True
        self.worker.start()

        # Check the tx queue for updates every 0.5 seconds
        self._event = Clock.schedule_interval(self.read_worker, 0.5)

    def stop_worker(self, *args, **kwargs):
        self.rx.put('STOP')
        print('Send poison pill')
        self.worker.join()
        print('All worker dead')

        #: Stop update loop
        if self._event:
            self._event.cancel()

        print('ID of labels in Kivy: {}'.format(id(self.sensor_labels)))
        print('Label text in Kivy:')
        for label in self.sensor_labels:
            print(label.text)


class DataScreen(Screen):

    def __init__(self, **kwargs):
        layout = StackLayout()
        super(DataScreen, self).__init__(**kwargs)
        self.n_probes = 8
        self.label_text = []
        for i in range(self.n_probes):
            self.label_text.append(StringProperty())
            self.label_text[i] = str(i)
        self.l = []
        for i in range(self.n_probes):
            self.l.append(Label(id='l_{}'.format(i),
                          text='Start {}'.format(i),
                          font_size='60sp',
                          height=20,
                          width=20,
                          size_hint=(0.5, 0.2)))
            self.ids.stack.add_widget(self.l[i])

    def change_text(self):
            for item in self.l:
                item.text = 'Update'


Builder.load_file('phapp.kv')

class MyApp(App):
    """
    The settings App is the main app of the pHBot application.
    It is initiated by kivy and contains the functions defining the main interface.
    """

    def build(self):
        """
        This function initializes the app interface and has to be called "build(self)".
        It returns the user interface defined by the Builder.
        """

        sm = ScreenManager()
        sm.add_widget(MainScreen())
        sm.add_widget(DataScreen())
        # returns the user interface defined by the Builder
        return sm

if __name__ == '__main__':
    MyApp().run()

Also the multiprocessing.Queue class is already 'process' safe, you don't need to use a lock around it. If you have a separate process for each sensor you can use the same idea just more queues.

like image 109
frmdstryr Avatar answered Oct 06 '22 01:10

frmdstryr