refactor service / schedule code in preparation for alternate schedulers
This commit is contained in:
parent
470fc43646
commit
12be082523
@ -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
174
owrx/service/schedule.py
Normal 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
|
2
setup.py
2
setup.py
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user