2022-06-01 16:37:19 +00:00
|
|
|
import logging
|
|
|
|
|
|
|
|
|
|
from lib.helpers import timing
|
|
|
|
|
from PyQt5.QtCore import QObject, QSemaphore, Qt, QTimer, pyqtSignal
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Component(QObject):
|
2022-06-21 12:10:52 +00:00
|
|
|
"""emitted with data from the _get method"""
|
2022-06-01 16:37:19 +00:00
|
|
|
out = pyqtSignal(list)
|
|
|
|
|
_pause = pyqtSignal()
|
|
|
|
|
_resume = pyqtSignal()
|
|
|
|
|
_set_sources = pyqtSignal(dict)
|
|
|
|
|
_set_period = pyqtSignal(dict)
|
|
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
|
self,
|
|
|
|
|
config=None,
|
|
|
|
|
name=None,
|
2022-06-21 12:10:52 +00:00
|
|
|
period=None,
|
|
|
|
|
lazy=True,
|
2022-06-01 16:37:19 +00:00
|
|
|
paused=False,
|
|
|
|
|
threaded=True,
|
|
|
|
|
):
|
2022-06-21 12:10:52 +00:00
|
|
|
"""
|
|
|
|
|
parameters:
|
|
|
|
|
config: value for self.config, should be instance of lib.helpers.config_reader.ConfigReader
|
|
|
|
|
name: value for self.name, should be used to retrive component configuration section (self.config[self.name])
|
|
|
|
|
period: period in seconds for periodic calls to _get, set to None to disable
|
|
|
|
|
lazy: whether or not skip periodic _get calls if falling behind
|
|
|
|
|
paused: whether or not periodic calls to_get are paused
|
|
|
|
|
threaded: set this to tell the component if it should be thread synchronized or not(shoul be true if calls to methods are done from threads different from the component's one)
|
|
|
|
|
"""
|
2022-06-01 16:37:19 +00:00
|
|
|
super().__init__()
|
|
|
|
|
self.config = config
|
|
|
|
|
self.name = name if name is not None else str(id(self))
|
|
|
|
|
self._threaded = threaded
|
|
|
|
|
self._period = period
|
|
|
|
|
self._single_shot = lazy
|
|
|
|
|
self._paused = paused
|
|
|
|
|
self._started = False
|
|
|
|
|
self._running = False
|
|
|
|
|
self.sources = {}
|
|
|
|
|
if self._threaded:
|
|
|
|
|
self._lock = QSemaphore(1)
|
|
|
|
|
self._lock.acquire(max(self._lock.available(), 1))
|
|
|
|
|
self._timer = None
|
|
|
|
|
self.log = logging.getLogger(f"{self.__class__.__name__} ({self.name})")
|
|
|
|
|
if not self._threaded:
|
|
|
|
|
self.start()
|
|
|
|
|
|
|
|
|
|
def _config_changed(self):
|
|
|
|
|
self.log.info("reconfigure")
|
|
|
|
|
self.config_changed()
|
|
|
|
|
self.log.debug(f"config: {self.config}")
|
|
|
|
|
|
|
|
|
|
def config_changed(self):
|
2022-06-21 12:10:52 +00:00
|
|
|
"""
|
|
|
|
|
this method should be overridden when inheriting from the Component class
|
|
|
|
|
and should contain all the initialization code that needs to access self.config
|
|
|
|
|
so that the component will reinitialize if configuration changes
|
|
|
|
|
this method will be called on start and when self.config (ConfigReader) emits the updated signal
|
|
|
|
|
"""
|
2022-06-01 16:37:19 +00:00
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
def start(self):
|
2022-06-21 12:10:52 +00:00
|
|
|
"""
|
|
|
|
|
this method is automatically called if threaded is set to False at object creation
|
|
|
|
|
otherwise if the component is in a thread this method should be used like this:
|
|
|
|
|
component = ComponentSubclass(threaded=True)
|
|
|
|
|
thread = QThread()
|
|
|
|
|
thread.setTerminationEnabled(True)
|
|
|
|
|
component.moveToThread(thread)
|
|
|
|
|
thread.started.connect(component.start)
|
|
|
|
|
thread.start()
|
|
|
|
|
component.wait_ready() # this is optional and will wait untill the component has finished started
|
|
|
|
|
"""
|
2022-06-01 16:37:19 +00:00
|
|
|
self._pause.connect(self._do_pause)
|
|
|
|
|
self._resume.connect(self._do_resume)
|
|
|
|
|
self._set_sources.connect(self._do_set_sources)
|
|
|
|
|
self._set_period.connect(self._do_set_period)
|
|
|
|
|
self.config.updated.connect(self._config_changed)
|
|
|
|
|
self._config_changed()
|
|
|
|
|
self._init_periodic()
|
|
|
|
|
self._started = True
|
|
|
|
|
if not self._paused:
|
|
|
|
|
self._do_resume()
|
|
|
|
|
elif self._threaded:
|
|
|
|
|
self._lock.release()
|
|
|
|
|
self.log.info("started")
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def started(self):
|
2022-06-21 12:10:52 +00:00
|
|
|
"""returns True if the component has been started"""
|
2022-06-01 16:37:19 +00:00
|
|
|
if self._threaded:
|
|
|
|
|
self._lock.acquire(max(self._lock.available(), 1))
|
|
|
|
|
started = self._started
|
|
|
|
|
if self._threaded:
|
|
|
|
|
self._lock.release()
|
|
|
|
|
return started
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def running(self):
|
2022-06-21 12:10:52 +00:00
|
|
|
"""returns True if the periodic calls to _get are not paused"""
|
2022-06-01 16:37:19 +00:00
|
|
|
if self._threaded:
|
|
|
|
|
self._lock.acquire(max(self._lock.available(), 1))
|
|
|
|
|
running = self._running
|
|
|
|
|
if self._threaded:
|
|
|
|
|
self._lock.release()
|
|
|
|
|
return running
|
|
|
|
|
|
|
|
|
|
def wait_ready(self, timeout=5):
|
2022-06-21 12:10:52 +00:00
|
|
|
"""
|
|
|
|
|
waits untill the requested action has been completed by the component
|
|
|
|
|
this will return immediately if threaded=False was passed at component initialization
|
|
|
|
|
"""
|
2022-06-01 16:37:19 +00:00
|
|
|
if self._threaded:
|
|
|
|
|
timeout = round(timeout * 1000)
|
|
|
|
|
if self._lock.tryAcquire(max(self._lock.available(), 1), timeout):
|
|
|
|
|
self._lock.release()
|
|
|
|
|
else:
|
|
|
|
|
self._lock.release()
|
|
|
|
|
raise RuntimeError(f"{self.name} was not ready before timeout of {timeout}ms")
|
|
|
|
|
|
|
|
|
|
def pause(self):
|
2022-06-21 12:10:52 +00:00
|
|
|
"""will pause periodic calls to _get and sources trigghers"""
|
2022-06-01 16:37:19 +00:00
|
|
|
if self._threaded:
|
|
|
|
|
self._lock.acquire(max(self._lock.available(), 1))
|
|
|
|
|
if self._running is False:
|
|
|
|
|
if self._threaded:
|
|
|
|
|
self._lock.release()
|
|
|
|
|
return
|
|
|
|
|
if self._threaded:
|
|
|
|
|
self._pause.emit()
|
|
|
|
|
self.wait_ready()
|
|
|
|
|
else:
|
|
|
|
|
self._do_pause()
|
|
|
|
|
|
|
|
|
|
def resume(self):
|
2022-06-21 12:10:52 +00:00
|
|
|
"""will resume periodic calls to _get and sources trigghers"""
|
2022-06-01 16:37:19 +00:00
|
|
|
if self._threaded:
|
|
|
|
|
self._lock.acquire(max(self._lock.available(), 1))
|
|
|
|
|
if self._running is True:
|
|
|
|
|
if self._threaded:
|
|
|
|
|
self._lock.release()
|
|
|
|
|
return
|
|
|
|
|
if self._threaded:
|
|
|
|
|
self._resume.emit()
|
|
|
|
|
self.wait_ready()
|
|
|
|
|
else:
|
|
|
|
|
self._do_resume()
|
|
|
|
|
|
|
|
|
|
def set_sources(self, sources=None): # sources should be {"source_name": signal_to_connect}
|
2022-06-21 12:10:52 +00:00
|
|
|
"""
|
|
|
|
|
connect the given sources to trigger a call to _get
|
|
|
|
|
the sources parameter should be:
|
|
|
|
|
a dict of signals might containing one optional argument that will be passed as data to _get
|
|
|
|
|
or None to disconnect all sources
|
|
|
|
|
"""
|
2022-06-01 16:37:19 +00:00
|
|
|
if self._threaded:
|
|
|
|
|
self._lock.acquire(max(self._lock.available(), 1))
|
|
|
|
|
self._set_sources.emit(sources)
|
|
|
|
|
self.wait_ready()
|
|
|
|
|
else:
|
|
|
|
|
self._do_set_sources(sources)
|
|
|
|
|
|
|
|
|
|
def _init_periodic(self):
|
|
|
|
|
if self._period is not None:
|
|
|
|
|
if self._timer is None:
|
|
|
|
|
self._timer = QTimer()
|
|
|
|
|
self._timer.setTimerType(Qt.PreciseTimer)
|
|
|
|
|
self._timer.setSingleShot(self._single_shot)
|
|
|
|
|
self._timer.setInterval(round(self._period * 1000))
|
|
|
|
|
self.log.debug(f"init periodic: period: {self._period}, single shot: {self._single_shot}")
|
|
|
|
|
else:
|
|
|
|
|
self.log.debug("no init periodic")
|
|
|
|
|
|
|
|
|
|
def set_period(self, period=None, lazy=True):
|
2022-06-21 12:10:52 +00:00
|
|
|
"""will set the period for periodic calls to _get and whether or not those are lazy (see init parameters)"""
|
2022-06-01 16:37:19 +00:00
|
|
|
if self._threaded:
|
|
|
|
|
self._lock.acquire(max(self._lock.available(), 1))
|
|
|
|
|
self._set_sources.emit({"period": period, "lazy": lazy})
|
|
|
|
|
self.wait_ready()
|
|
|
|
|
else:
|
|
|
|
|
self._do_set_period({"period": period, "lazy": lazy})
|
|
|
|
|
|
|
|
|
|
def _start_periodic(self):
|
|
|
|
|
if self._timer is not None:
|
|
|
|
|
self._timer.timeout.connect(self._get)
|
|
|
|
|
self._timer.start()
|
|
|
|
|
self.log.debug(f"started periodic: {list(self.sources)}")
|
|
|
|
|
else:
|
|
|
|
|
self.log.debug("no started periodic")
|
|
|
|
|
|
|
|
|
|
def _stop_periodic(self):
|
|
|
|
|
if self._timer is not None:
|
|
|
|
|
self._timer.stop()
|
|
|
|
|
try:
|
|
|
|
|
self._timer.timeout.disconnect()
|
|
|
|
|
except TypeError:
|
|
|
|
|
pass
|
|
|
|
|
self.log.debug(f"stopped periodic: {list(self.sources)}")
|
|
|
|
|
else:
|
|
|
|
|
self.log.debug("no stopped periodic")
|
|
|
|
|
|
|
|
|
|
def _connect_sources(self):
|
|
|
|
|
if self.sources is not None:
|
|
|
|
|
for source in self.sources.values():
|
|
|
|
|
source.connect(self._get)
|
|
|
|
|
self.log.debug(f"connected sources: {list(self.sources)}")
|
|
|
|
|
else:
|
|
|
|
|
self.log.debug("no connected sources")
|
|
|
|
|
|
|
|
|
|
def _disconnect_sources(self):
|
|
|
|
|
if self.sources is not None:
|
|
|
|
|
for source in self.sources.values():
|
|
|
|
|
try:
|
|
|
|
|
source.disconnect()
|
|
|
|
|
except TypeError:
|
|
|
|
|
pass
|
|
|
|
|
self.log.debug(f"disconnected sources: {list(self.sources)}")
|
|
|
|
|
else:
|
|
|
|
|
self.log.debug("no disconnected sources")
|
|
|
|
|
|
|
|
|
|
def _do_resume(self):
|
|
|
|
|
self._start_periodic()
|
|
|
|
|
self._connect_sources()
|
|
|
|
|
self._running = True
|
|
|
|
|
self.log.info("resumed")
|
|
|
|
|
if self._threaded:
|
|
|
|
|
self._lock.release()
|
|
|
|
|
|
|
|
|
|
def _do_pause(self):
|
|
|
|
|
self._stop_periodic()
|
|
|
|
|
self._disconnect_sources()
|
|
|
|
|
self._running = False
|
|
|
|
|
self.log.info("paused")
|
|
|
|
|
if self._threaded:
|
|
|
|
|
self._lock.release()
|
|
|
|
|
|
|
|
|
|
def _do_set_sources(self, sources):
|
|
|
|
|
if self._running:
|
|
|
|
|
self._disconnect_sources()
|
|
|
|
|
self.sources = sources
|
|
|
|
|
if self._running:
|
|
|
|
|
self._connect_sources()
|
|
|
|
|
self.log.info("set sources")
|
|
|
|
|
if self._threaded:
|
|
|
|
|
self._lock.release()
|
|
|
|
|
|
|
|
|
|
def _do_set_period(self, spec):
|
|
|
|
|
self._period = spec.get("period", None)
|
|
|
|
|
self._single_shot = spec.get("lazy", True)
|
|
|
|
|
self._init_periodic()
|
|
|
|
|
self.log.info("set period")
|
|
|
|
|
if self._threaded:
|
|
|
|
|
self._lock.release()
|
|
|
|
|
|
|
|
|
|
def _get(self, data=None):
|
2022-06-21 12:10:52 +00:00
|
|
|
"""
|
|
|
|
|
this method should be overridden when inheriting from the Component class
|
|
|
|
|
the overriding method should retrive all the data and then call super()._get(data)
|
|
|
|
|
this will emit the data in the proper format
|
|
|
|
|
"""
|
2022-06-01 16:37:19 +00:00
|
|
|
if data is None:
|
|
|
|
|
data = [None]
|
2022-06-21 12:10:52 +00:00
|
|
|
t = timing()
|
|
|
|
|
got = [{"time": t, self.name: d} for d in data]
|
2022-06-01 16:37:19 +00:00
|
|
|
self.out.emit(got)
|
|
|
|
|
self.log.debug(f"_get: {got}")
|
2022-06-21 12:10:52 +00:00
|
|
|
if self._timer is not None and self._single_shot:
|
2022-06-01 16:37:19 +00:00
|
|
|
self._timer.start()
|
|
|
|
|
|
|
|
|
|
def set(self, val):
|
2022-06-21 12:10:52 +00:00
|
|
|
"""
|
|
|
|
|
this method should be overridden when inheriting from the Component class
|
|
|
|
|
the overriding method should set the requested val and then call super()._set(set_value)
|
|
|
|
|
this will log the value that has been set
|
|
|
|
|
"""
|
2022-06-01 16:37:19 +00:00
|
|
|
self.log.debug(f"set: {val}")
|