refactor service / schedule code in preparation for alternate schedulers

This commit is contained in:
Jakob Ketterl 2020-01-17 22:46:01 +01:00
parent 470fc43646
commit 12be082523
3 changed files with 182 additions and 142 deletions

View File

@ -1,5 +1,4 @@
import threading import threading
from datetime import datetime, timezone, timedelta
from owrx.source import SdrSource from owrx.source import SdrSource
from owrx.sdr import SdrService from owrx.sdr import SdrService
from owrx.bands import Bandplan from owrx.bands import Bandplan
@ -9,16 +8,19 @@ from owrx.aprs import AprsParser
from owrx.config import PropertyManager from owrx.config import PropertyManager
from owrx.source.resampler import Resampler from owrx.source.resampler import Resampler
from owrx.feature import FeatureDetector from owrx.feature import FeatureDetector
from abc import ABCMeta, abstractmethod
from .schedule import ServiceScheduler
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class ServiceOutput(output): class ServiceOutput(output, metaclass=ABCMeta):
def __init__(self, frequency): def __init__(self, frequency):
self.frequency = frequency self.frequency = frequency
@abstractmethod
def getParser(self): def getParser(self):
# abstract method; implement in subclasses # abstract method; implement in subclasses
pass pass
@ -46,134 +48,6 @@ class AprsServiceOutput(ServiceOutput):
return t == "packet_demod" return t == "packet_demod"
class ScheduleEntry(object):
def __init__(self, startTime, endTime, profile):
self.startTime = startTime
self.endTime = endTime
self.profile = profile
def isCurrent(self, time):
if self.startTime < self.endTime:
return self.startTime <= time < self.endTime
else:
return self.startTime <= time or time < self.endTime
def getProfile(self):
return self.profile
def getScheduledEnd(self):
now = datetime.utcnow()
end = now.combine(date=now.date(), time=self.endTime)
while end < now:
end += timedelta(days=1)
return end
def getNextActivation(self):
now = datetime.utcnow()
start = now.combine(date=now.date(), time=self.startTime)
while start < now:
start += timedelta(days=1)
return start
class Schedule(object):
@staticmethod
def parse(scheduleDict):
entries = []
for time, profile in scheduleDict.items():
if len(time) != 9:
logger.warning("invalid schedule spec: %s", time)
continue
startTime = datetime.strptime(time[0:4], "%H%M").replace(tzinfo=timezone.utc).time()
endTime = datetime.strptime(time[5:9], "%H%M").replace(tzinfo=timezone.utc).time()
entries.append(ScheduleEntry(startTime, endTime, profile))
return Schedule(entries)
def __init__(self, entries):
self.entries = entries
def getCurrentEntry(self):
current = [p for p in self.entries if p.isCurrent(datetime.utcnow().time())]
if current:
return current[0]
return None
def getNextEntry(self):
s = sorted(self.entries, key=lambda e: e.getNextActivation())
if s:
return s[0]
return None
class ServiceScheduler(object):
def __init__(self, source, schedule):
self.source = source
self.selectionTimer = None
self.schedule = Schedule.parse(schedule)
self.source.addClient(self)
self.source.getProps().collect("center_freq", "samp_rate").wire(self.onFrequencyChange)
self.scheduleSelection()
def shutdown(self):
self.cancelTimer()
self.source.removeClient(self)
def scheduleSelection(self, time=None):
if self.source.getState() == SdrSource.STATE_FAILED:
return
seconds = 10
if time is not None:
delta = time - datetime.utcnow()
seconds = delta.total_seconds()
self.cancelTimer()
self.selectionTimer = threading.Timer(seconds, self.selectProfile)
self.selectionTimer.start()
def cancelTimer(self):
if self.selectionTimer:
self.selectionTimer.cancel()
def getClientClass(self):
return SdrSource.CLIENT_BACKGROUND
def onStateChange(self, state):
if state == SdrSource.STATE_STOPPING:
self.scheduleSelection()
elif state == SdrSource.STATE_FAILED:
self.cancelTimer()
def onBusyStateChange(self, state):
if state == SdrSource.BUSYSTATE_IDLE:
self.scheduleSelection()
def onFrequencyChange(self, name, value):
self.scheduleSelection()
def selectProfile(self):
if self.source.hasClients(SdrSource.CLIENT_USER):
logger.debug("source has active users; not touching")
return
logger.debug("source seems to be idle, selecting profile for background services")
entry = self.schedule.getCurrentEntry()
if entry is None:
logger.debug("schedule did not return a profile. checking next entry...")
nextEntry = self.schedule.getNextEntry()
if nextEntry is not None:
self.scheduleSelection(nextEntry.getNextActivation())
return
logger.debug("scheduling end for current profile: %s", entry.getScheduledEnd())
self.scheduleSelection(entry.getScheduledEnd())
try:
self.source.activateProfile(entry.getProfile())
self.source.start()
except KeyError:
pass
class ServiceHandler(object): class ServiceHandler(object):
def __init__(self, source): def __init__(self, source):
self.lock = threading.Lock() self.lock = threading.Lock()
@ -186,8 +60,8 @@ class ServiceHandler(object):
if self.source.isAvailable(): if self.source.isAvailable():
self.scheduleServiceStartup() self.scheduleServiceStartup()
self.scheduler = None self.scheduler = None
if "schedule" in props: if "schedule" in props or "scheduler" in props:
self.scheduler = ServiceScheduler(self.source, props["schedule"]) self.scheduler = ServiceScheduler(self.source)
def getClientClass(self): def getClientClass(self):
return SdrSource.CLIENT_INACTIVE return SdrSource.CLIENT_INACTIVE
@ -390,7 +264,7 @@ class Services(object):
return return
for source in SdrService.getSources().values(): for source in SdrService.getSources().values():
props = source.getProps() props = source.getProps()
if "services" not in props or props["services"] != False: if "services" not in props or props["services"] is not False:
Services.handlers.append(ServiceHandler(source)) Services.handlers.append(ServiceHandler(source))
@staticmethod @staticmethod
@ -398,11 +272,3 @@ class Services(object):
for handler in Services.handlers: for handler in Services.handlers:
handler.shutdown() handler.shutdown()
Services.handlers = [] Services.handlers = []
class Service(object):
pass
class WsjtService(Service):
pass

174
owrx/service/schedule.py Normal file
View File

@ -0,0 +1,174 @@
from datetime import datetime, timezone, timedelta
from owrx.source import SdrSource
import threading
from abc import ABC, ABCMeta, abstractmethod
import logging
logger = logging.getLogger(__name__)
class ScheduleEntry(object):
def __init__(self, startTime, endTime, profile):
self.startTime = startTime
self.endTime = endTime
self.profile = profile
def isCurrent(self, time):
if self.startTime < self.endTime:
return self.startTime <= time < self.endTime
else:
return self.startTime <= time or time < self.endTime
def getProfile(self):
return self.profile
def getScheduledEnd(self):
now = datetime.utcnow()
end = now.combine(date=now.date(), time=self.endTime)
while end < now:
end += timedelta(days=1)
return end
def getNextActivation(self):
now = datetime.utcnow()
start = now.combine(date=now.date(), time=self.startTime)
while start < now:
start += timedelta(days=1)
return start
class Schedule(ABC):
@staticmethod
def parse(props):
# downwards compatibility
if "schedule" in props:
return StaticSchedule(props["schedule"])
elif "scheduler" in props:
sc = props["scheduler"]
t = sc["type"] if "type" in sc else "static"
if t == "static":
return StaticSchedule(sc["schedule"])
elif t == "sunlight":
return SunlightSchedule(sc["schedule"])
else:
logger.warning("Invalid scheduler type: %s", t)
@abstractmethod
def getCurrentEntry(self):
pass
@abstractmethod
def getNextEntry(self):
pass
class TimerangeSchedule(Schedule, metaclass=ABCMeta):
@abstractmethod
def getEntries(self):
pass
def getCurrentEntry(self):
current = [p for p in self.getEntries() if p.isCurrent(datetime.utcnow().time())]
if current:
return current[0]
return None
def getNextEntry(self):
s = sorted(self.getEntries(), key=lambda e: e.getNextActivation())
if s:
return s[0]
return None
class StaticSchedule(TimerangeSchedule):
def __init__(self, scheduleDict):
self.entries = []
for time, profile in scheduleDict.items():
if len(time) != 9:
logger.warning("invalid schedule spec: %s", time)
continue
startTime = datetime.strptime(time[0:4], "%H%M").replace(tzinfo=timezone.utc).time()
endTime = datetime.strptime(time[5:9], "%H%M").replace(tzinfo=timezone.utc).time()
self.entries.append(ScheduleEntry(startTime, endTime, profile))
def getEntries(self):
return self.entries
class SunlightSchedule(TimerangeSchedule):
def __init__(self, scheduleDict):
self.schedule = scheduleDict
def getEntries(self):
return []
class ServiceScheduler(object):
def __init__(self, source):
self.source = source
self.selectionTimer = None
self.source.addClient(self)
props = self.source.getProps()
self.schedule = Schedule.parse(props)
props.collect("center_freq", "samp_rate").wire(self.onFrequencyChange)
self.scheduleSelection()
def shutdown(self):
self.cancelTimer()
self.source.removeClient(self)
def scheduleSelection(self, time=None):
if self.source.getState() == SdrSource.STATE_FAILED:
return
seconds = 10
if time is not None:
delta = time - datetime.utcnow()
seconds = delta.total_seconds()
self.cancelTimer()
self.selectionTimer = threading.Timer(seconds, self.selectProfile)
self.selectionTimer.start()
def cancelTimer(self):
if self.selectionTimer:
self.selectionTimer.cancel()
def getClientClass(self):
return SdrSource.CLIENT_BACKGROUND
def onStateChange(self, state):
if state == SdrSource.STATE_STOPPING:
self.scheduleSelection()
elif state == SdrSource.STATE_FAILED:
self.cancelTimer()
def onBusyStateChange(self, state):
if state == SdrSource.BUSYSTATE_IDLE:
self.scheduleSelection()
def onFrequencyChange(self, name, value):
self.scheduleSelection()
def selectProfile(self):
if self.source.hasClients(SdrSource.CLIENT_USER):
logger.debug("source has active users; not touching")
return
logger.debug("source seems to be idle, selecting profile for background services")
entry = self.schedule.getCurrentEntry()
if entry is None:
logger.debug("schedule did not return a profile. checking next entry...")
nextEntry = self.schedule.getNextEntry()
if nextEntry is not None:
self.scheduleSelection(nextEntry.getNextActivation())
return
logger.debug("scheduling end for current profile: %s", entry.getScheduledEnd())
self.scheduleSelection(entry.getScheduledEnd())
try:
self.source.activateProfile(entry.getProfile())
self.source.start()
except KeyError:
pass

View File

@ -11,7 +11,7 @@ except ImportError:
setup( setup(
name="OpenWebRX", name="OpenWebRX",
version=str(strictversion), version=str(strictversion),
packages=find_namespace_packages(include=["owrx", "owrx.source", "csdr", "htdocs"]), packages=find_namespace_packages(include=["owrx", "owrx.source", "owrx.service", "csdr", "htdocs"]),
package_data={"htdocs": [f[len("htdocs/") :] for f in glob("htdocs/**/*", recursive=True)]}, package_data={"htdocs": [f[len("htdocs/") :] for f in glob("htdocs/**/*", recursive=True)]},
entry_points={"console_scripts": ["openwebrx=owrx.__main__:main"]}, entry_points={"console_scripts": ["openwebrx=owrx.__main__:main"]},
# use the github page for now # use the github page for now