st-ten-1/src/components/component.py

330 lines
12 KiB
Python
Raw Normal View History

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
2022-06-22 15:18:29 +00:00
self.sources = None
2022-06-01 16:37:19 +00:00
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)
2022-06-22 15:18:29 +00:00
if self.config is not None:
self.config.updated.connect(self._config_changed)
2022-06-01 16:37:19 +00:00
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()
2022-06-22 15:18:29 +00:00
def set_sources(self, sources=None): #
2022-06-21 12:10:52 +00:00
"""
2022-06-22 15:18:29 +00:00
connect the given sources to trigger a call to _get,
2022-06-21 12:10:52 +00:00
the sources parameter should be:
2022-06-22 15:18:29 +00:00
a dict of signals ({"<source_name>": <signal_to_connect>, ...})
signals might contain one optional argument that will be passed as data to _get
2022-06-21 12:10:52 +00:00
or None to disconnect all sources
"""
2022-06-28 10:31:27 +00:00
if sources is None:
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)
2022-06-22 15:18:29 +00:00
def add_sources(self, sources=None, overwrite_conflicting_sources=False): # sources should be {"source_name": signal_to_connect, ...}
"""
add the given sources to the current ones,
the sources parameter should be:
a dict of signals ({"<source_name>": <signal_to_connect>, ...})
signals might contain one optional argument that will be passed as data to _get
or None if no sources are to be added
this method calls set_sources, this is semplest but not the most efficient approach
"""
if self.sources is None:
self_sources = {}
else:
self_sources = self.sources
if sources is None:
sources = {}
if not overwrite_conflicting_sources:
conflicting_sources = {
n: [self_sources[n], sources[n]]
for n in self_sources.keys() & sources.keys()
if self_sources[n] is not sources[n]
}
if len(conflicting_sources):
2022-06-28 10:31:27 +00:00
raise AssertionError("\n\t" + "\n\t".join([f"source named {n!r}: {s[0]!r} will not be replaced with {s[1]!r}" for n, s in conflicting_sources.items()]))
2022-06-22 15:18:29 +00:00
self.set_sources({**self_sources, **sources})
def remove_sources(self, sources=None):
"""
remove the given sources to the current ones by name,
the sources parameter should be:
an iterable of source names (["<source_name>", ...])
or None if no sources are to be removed
this method calls set_sources, this is semplest but not the most efficient approach
"""
2022-06-28 10:31:27 +00:00
if sources is None or self.sources is None:
2022-06-22 15:18:29 +00:00
return
sources = set(sources)
self.set_sources({n: s for n, s in self.sources.items() if n not in sources})
2022-06-01 16:37:19 +00:00
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))
2022-06-22 15:18:29 +00:00
self._set_period.emit({"period": period, "lazy": lazy})
2022-06-01 16:37:19 +00:00
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()
2022-06-22 15:18:29 +00:00
self.log.debug("started periodic")
2022-06-01 16:37:19 +00:00
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
2022-06-22 15:18:29 +00:00
self.log.debug("stopped periodic")
2022-06-01 16:37:19 +00:00
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()
2022-06-28 10:31:27 +00:00
if sources is not None and not len(sources):
sources = None
2022-06-01 16:37:19 +00:00
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()
2022-06-22 15:18:29 +00:00
def _get(self, data=None, emit=True):
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)
2022-06-22 15:18:29 +00:00
this will emit the data in the proper format if the emit parameter is not set to False
2022-06-21 12:10:52 +00:00
"""
2022-06-22 15:18:29 +00:00
if emit:
if data is None:
data = [None]
t = timing()
got = [{
"time": t if type(d) is not dict or "time" not in d else d["time"],
self.name: d,
} for d in data]
self.out.emit(got)
self.log.debug(f"_get: {got}")
else:
self.log.debug("_get")
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}")