rewrite property engine

Property class is gone; logic is now done with Layers, Stack and Filter
This commit is contained in:
Jakob Ketterl 2020-03-23 23:56:05 +01:00
parent 7562dc8ecb
commit c83d8580ba
12 changed files with 298 additions and 176 deletions

View File

@ -1,4 +1,4 @@
from owrx.property import PropertyManager
from owrx.property import PropertyLayer
import importlib.util
import os
import logging
@ -24,7 +24,7 @@ class Config:
@staticmethod
def _loadConfig():
pm = PropertyManager()
pm = PropertyLayer()
for file in ["/etc/openwebrx/config_webrx.py", "./config_webrx.py"]:
try:
spec = importlib.util.spec_from_file_location("config_webrx", file)
@ -55,7 +55,7 @@ class Config:
return [e for e in errors if e is not None]
@staticmethod
def checkTempDirectory(pm: PropertyManager):
def checkTempDirectory(pm: PropertyLayer):
key = "temporary_directory"
if key not in pm or pm[key] is None:
return ConfigError(key, "temporary directory is not set")

View File

@ -10,6 +10,7 @@ from owrx.bands import Bandplan
from owrx.bookmarks import Bookmarks
from owrx.map import Map
from owrx.locator import Locator
from owrx.property import PropertyStack
from multiprocessing import Queue
from queue import Full
import json
@ -195,14 +196,14 @@ class OpenWebRxReceiverClient(Client):
# send initial config
self.setDspProperties(self.connectionProperties)
configProps = (
self.sdr.getProps()
.collect(*OpenWebRxReceiverClient.config_keys)
.defaults(Config.get())
)
stack = PropertyStack()
stack.addLayer(0, self.sdr.getProps())
stack.addLayer(1, Config.get())
configProps = stack.collect(*OpenWebRxReceiverClient.config_keys)
def sendConfig(key, value):
config = dict((key, configProps[key]) for key in OpenWebRxReceiverClient.config_keys)
#config = dict((key, configProps[key]) for key in OpenWebRxReceiverClient.config_keys)
config = configProps.__dict__()
# TODO mathematical properties? hmmmm
config["start_offset_freq"] = configProps["start_freq"] - configProps["center_freq"]
# TODO this is a hack to support multiple sdrs
@ -253,11 +254,10 @@ class OpenWebRxReceiverClient(Client):
if not keys:
return
# only the keys in the protected property manager can be overridden from the web
protected = (
self.sdr.getProps()
.collect(*keys)
.defaults(config)
)
stack = PropertyStack()
stack.addLayer(0, self.sdr.getProps())
stack.addLayer(1, config)
protected = stack.collect(*keys)
for key, value in params.items():
protected[key] = value

View File

@ -4,6 +4,7 @@ from owrx.wsjt import WsjtParser
from owrx.aprs import AprsParser
from owrx.pocsag import PocsagParser
from owrx.source import SdrSource
from owrx.property import PropertyStack
from csdr import csdr
import threading
@ -23,23 +24,30 @@ class DspManager(csdr.output):
"pocsag_demod": PocsagParser(self.handler),
}
self.localProps = (
self.sdrSource.getProps()
.collect(
"audio_compression",
"fft_compression",
"digimodes_fft_size",
"csdr_dynamic_bufsize",
"csdr_print_bufsizes",
"csdr_through",
"digimodes_enable",
"samp_rate",
"digital_voice_unvoiced_quality",
"dmr_filter",
"temporary_directory",
"center_freq",
)
.defaults(Config.get())
stack = PropertyStack()
stack.addLayer(0, self.sdrSource.getProps())
stack.addLayer(1, Config.get())
self.localProps = stack.collect(
"audio_compression",
"fft_compression",
"digimodes_fft_size",
"csdr_dynamic_bufsize",
"csdr_print_bufsizes",
"csdr_through",
"digimodes_enable",
"samp_rate",
"digital_voice_unvoiced_quality",
"dmr_filter",
"temporary_directory",
"center_freq",
# TODO: following properties are set from the client
"output_rate",
"squelch_level",
"secondary_mod",
"low_cut",
"high_cut",
"offset_freq",
)
self.dsp = csdr.dsp(self)
@ -61,19 +69,19 @@ class DspManager(csdr.output):
parser.setDialFrequency(freq)
self.subscriptions = [
self.localProps.getProperty("audio_compression").wire(self.dsp.set_audio_compression),
self.localProps.getProperty("fft_compression").wire(self.dsp.set_fft_compression),
self.localProps.getProperty("digimodes_fft_size").wire(self.dsp.set_secondary_fft_size),
self.localProps.getProperty("samp_rate").wire(self.dsp.set_samp_rate),
self.localProps.getProperty("output_rate").wire(self.dsp.set_output_rate),
self.localProps.getProperty("offset_freq").wire(self.dsp.set_offset_freq),
self.localProps.getProperty("squelch_level").wire(self.dsp.set_squelch_level),
self.localProps.getProperty("low_cut").wire(set_low_cut),
self.localProps.getProperty("high_cut").wire(set_high_cut),
self.localProps.getProperty("mod").wire(self.dsp.set_demodulator),
self.localProps.getProperty("digital_voice_unvoiced_quality").wire(self.dsp.set_unvoiced_quality),
self.localProps.getProperty("dmr_filter").wire(self.dsp.set_dmr_filter),
self.localProps.getProperty("temporary_directory").wire(self.dsp.set_temporary_directory),
self.localProps.wireProperty("audio_compression", self.dsp.set_audio_compression),
self.localProps.wireProperty("fft_compression", self.dsp.set_fft_compression),
self.localProps.wireProperty("digimodes_fft_size", self.dsp.set_secondary_fft_size),
self.localProps.wireProperty("samp_rate", self.dsp.set_samp_rate),
self.localProps.wireProperty("output_rate", self.dsp.set_output_rate),
self.localProps.wireProperty("offset_freq", self.dsp.set_offset_freq),
self.localProps.wireProperty("squelch_level", self.dsp.set_squelch_level),
self.localProps.wireProperty("low_cut", set_low_cut),
self.localProps.wireProperty("high_cut", set_high_cut),
self.localProps.wireProperty("mod", self.dsp.set_demodulator),
self.localProps.wireProperty("digital_voice_unvoiced_quality", self.dsp.set_unvoiced_quality),
self.localProps.wireProperty("dmr_filter", self.dsp.set_dmr_filter),
self.localProps.wireProperty("temporary_directory", self.dsp.set_temporary_directory),
self.localProps.collect("center_freq", "offset_freq").wire(set_dial_freq),
]
@ -99,8 +107,8 @@ class DspManager(csdr.output):
)
self.subscriptions += [
self.localProps.getProperty("secondary_mod").wire(set_secondary_mod),
self.localProps.getProperty("secondary_offset_freq").wire(self.dsp.set_secondary_offset_freq),
self.localProps.collect("secondary_mod").wire(set_secondary_mod),
self.localProps.collect("secondary_offset_freq").wire(self.dsp.set_secondary_offset_freq),
]
self.sdrSource.addClient(self)
@ -134,7 +142,7 @@ class DspManager(csdr.output):
self.subscriptions = []
def setProperty(self, prop, value):
self.localProps.getProperty(prop).setValue(value)
self.localProps[prop] = value
def getClientClass(self):
return SdrSource.CLIENT_USER

View File

@ -2,6 +2,7 @@ from owrx.config import Config
from csdr import csdr
import threading
from owrx.source import SdrSource
from owrx.property import PropertyStack
import logging
@ -13,7 +14,10 @@ class SpectrumThread(csdr.output):
self.sdrSource = sdrSource
super().__init__()
self.props = props = self.sdrSource.props.collect(
stack = PropertyStack()
stack.addLayer(0, self.sdrSource.props)
stack.addLayer(1, Config.get())
self.props = props = stack.collect(
"samp_rate",
"fft_size",
"fft_fps",
@ -23,7 +27,7 @@ class SpectrumThread(csdr.output):
"csdr_print_bufsizes",
"csdr_through",
"temporary_directory",
).defaults(Config.get())
)
self.dsp = dsp = csdr.dsp(self)
dsp.nc_port = self.sdrSource.getPort()
@ -42,11 +46,11 @@ class SpectrumThread(csdr.output):
)
self.subscriptions = [
props.getProperty("samp_rate").wire(dsp.set_samp_rate),
props.getProperty("fft_size").wire(dsp.set_fft_size),
props.getProperty("fft_fps").wire(dsp.set_fft_fps),
props.getProperty("fft_compression").wire(dsp.set_fft_compression),
props.getProperty("temporary_directory").wire(dsp.set_temporary_directory),
props.wireProperty("samp_rate", dsp.set_samp_rate),
props.wireProperty("fft_size", dsp.set_fft_size),
props.wireProperty("fft_fps", dsp.set_fft_fps),
props.wireProperty("fft_compression", dsp.set_fft_compression),
props.wireProperty("temporary_directory", dsp.set_temporary_directory),
props.collect("samp_rate", "fft_size", "fft_fps", "fft_voverlap_factor").wire(set_fft_averages),
]

View File

@ -1,13 +1,18 @@
from abc import ABC, abstractmethod
import logging
logger = logging.getLogger(__name__)
class Subscription(object):
def __init__(self, subscriptee, subscriber):
def __init__(self, subscriptee, name, subscriber):
self.subscriptee = subscriptee
self.name = name
self.subscriber = subscriber
def getName(self):
return self.name
def call(self, *args, **kwargs):
self.subscriber(*args, **kwargs)
@ -15,30 +20,39 @@ class Subscription(object):
self.subscriptee.unwire(self)
class Property(object):
def __init__(self, value=None):
self.value = value
class PropertyManager(ABC):
def __init__(self):
self.subscribers = []
def getValue(self):
return self.value
@abstractmethod
def __getitem__(self, item):
pass
def setValue(self, value):
if self.value == value:
return self
self.value = value
for c in self.subscribers:
try:
c.call(self.value)
except Exception as e:
logger.exception(e)
return self
@abstractmethod
def __setitem__(self, key, value):
pass
@abstractmethod
def __contains__(self, item):
pass
@abstractmethod
def __dict__(self):
pass
def collect(self, *props):
return PropertyFilter(self, *props)
def wire(self, callback):
sub = Subscription(self, callback)
sub = Subscription(self, None, callback)
self.subscribers.append(sub)
if self.value is not None:
sub.call(self.value)
return sub
def wireProperty(self, name, callback):
sub = Subscription(self, name, callback)
self.subscribers.append(sub)
if name in self:
sub.call(self[name])
return sub
def unwire(self, sub):
@ -49,80 +63,79 @@ class Property(object):
pass
return self
def _fireCallbacks(self, name, value):
for c in self.subscribers:
try:
if c.getName() is None:
c.call(name, value)
elif c.getName() == name:
c.call(value)
except Exception as e:
logger.exception(e)
class PropertyManager(object):
def collect(self, *props):
return PropertyManager(
{name: self.getProperty(name) if self.hasProperty(name) else Property() for name in props}
)
class PropertyLayer(PropertyManager):
def __init__(self, properties=None):
super().__init__()
self.properties = {}
self.subscribers = []
if properties is not None:
for (name, prop) in properties.items():
self.add(name, prop)
self._add(name, prop)
def add(self, name, prop):
def _add(self, name, prop):
self.properties[name] = prop
def fireCallbacks(value):
for c in self.subscribers:
try:
c.call(name, value)
except Exception as e:
logger.exception(e)
prop.wire(fireCallbacks)
self._fireCallbacks(name, prop.getValue())
return self
def __contains__(self, name):
return self.hasProperty(name)
def __getitem__(self, name):
return self.getPropertyValue(name)
def __setitem__(self, name, value):
if not self.hasProperty(name):
self.add(name, Property())
self.getProperty(name).setValue(value)
def __dict__(self):
return {k: v.getValue() for k, v in self.properties.items()}
def hasProperty(self, name):
return name in self.properties
def getProperty(self, name):
if not self.hasProperty(name):
self.add(name, Property())
def __getitem__(self, name):
return self.properties[name]
def getPropertyValue(self, name):
return self.getProperty(name).getValue()
def __setitem__(self, name, value):
logger.debug("property change: %s => %s", name, value)
self.properties[name] = value
self._fireCallbacks(name, value)
def wire(self, callback):
sub = Subscription(self, callback)
self.subscribers.append(sub)
return sub
def unwire(self, sub):
try:
self.subscribers.remove(sub)
except ValueError:
# happens when already removed before
pass
return self
def defaults(self, other_pm):
for (key, p) in self.properties.items():
if p.getValue() is None:
p.setValue(other_pm[key])
return self
def __dict__(self):
return {k: v for k, v in self.properties.items()}
class PropertyLayers(object):
class PropertyFilter(PropertyManager):
def __init__(self, pm: PropertyManager, *props: str):
super().__init__()
self.pm = pm
self.props = props
self.pm.wire(self.receiveEvent)
def receiveEvent(self, name, value):
if name not in self.props:
return
self._fireCallbacks(name, value)
def __getitem__(self, item):
if item not in self.props:
raise KeyError(item)
return self.pm.__getitem__(item)
def __setitem__(self, key, value):
if key not in self.props:
raise KeyError(key)
return self.pm.__setitem__(key, value)
def __contains__(self, item):
if item not in self.props:
return False
return self.pm.__contains__(item)
def __dict__(self):
return {k: v for k, v in self.pm.__dict__().items() if k in self.props}
class PropertyStack(PropertyManager):
def __init__(self):
super().__init__()
self.layers = []
def addLayer(self, priority: int, pm: PropertyManager):
@ -136,8 +149,26 @@ class PropertyLayers(object):
if layer["props"] == pm:
self.layers.remove(layer)
def __getitem__(self, item):
def _getLayer(self, item):
layers = [la["props"] for la in sorted(self.layers, key=lambda l: l["priority"])]
for m in layers:
if item in m:
return m[item]
return m
# return top layer by default
return layers[0]
def __getitem__(self, item):
layer = self._getLayer(item)
return layer.__getitem__(item)
def __setitem__(self, key, value):
layer = self._getLayer(key)
return layer.__setitem__(key, value)
def __contains__(self, item):
layer = self._getLayer(item)
return layer.__contains__(item)
def __dict__(self):
keys = [key for l in self.layers for key in l["props"].__dict__().keys()]
return {k: self.__getitem__(k) for k in keys}

View File

@ -1,5 +1,5 @@
from owrx.config import Config
from owrx.property import PropertyManager
from owrx.property import PropertyLayer
from owrx.feature import FeatureDetector, UnknownFeatureException
import logging
@ -19,7 +19,7 @@ class SdrService(object):
featureDetector = FeatureDetector()
def loadIntoPropertyManager(dict: dict):
propertyManager = PropertyManager()
propertyManager = PropertyLayer()
for (name, value) in dict.items():
propertyManager[name] = value
return propertyManager

View File

@ -8,7 +8,7 @@ from owrx.aprs import AprsParser
from owrx.config import Config
from owrx.source.resampler import Resampler
from owrx.feature import FeatureDetector
from owrx.property import PropertyManager
from owrx.property import PropertyLayer
from abc import ABCMeta, abstractmethod
from .schedule import ServiceScheduler
@ -164,7 +164,7 @@ class ServiceHandler(object):
cf = (min + max) / 2
bw = max - min
logger.debug("group center frequency: {0}, bandwidth: {1}".format(cf, bw))
resampler_props = PropertyManager()
resampler_props = PropertyLayer()
resampler_props["center_freq"] = cf
# TODO the + 24000 is a temporary fix since the resampling optimizer does not account for required bandwidths
resampler_props["samp_rate"] = bw + 24000

View File

@ -9,6 +9,7 @@ import signal
from abc import ABC, abstractmethod
from owrx.command import CommandMapper
from owrx.socket import getAvailablePort
from owrx.property import PropertyStack
import logging
@ -35,7 +36,10 @@ class SdrSource(ABC):
self.props = props
self.profile_id = None
self.activateProfile()
self.rtlProps = self.props.collect(*self.getEventNames()).defaults(Config.get())
stack = PropertyStack()
stack.addLayer(0, self.props)
stack.addLayer(1, Config.get())
self.rtlProps = stack.collect(*self.getEventNames())
self.wireEvents()
self.commandMapper = None

View File

@ -1,28 +0,0 @@
import unittest
from unittest.mock import Mock
from owrx.property import Property
class PropertyTest(unittest.TestCase):
def testValue(self):
prop = Property("testvalue")
self.assertEqual(prop.getValue(), "testvalue")
def testChangeValue(self):
prop = Property("before")
prop.setValue("after")
self.assertEqual(prop.getValue(), "after")
def testInitialValueOnCallback(self):
prop = Property("before")
m = Mock()
prop.wire(m.method)
m.method.assert_called_once_with("before")
def testChangedValueOnCallback(self):
prop = Property("before")
m = Mock()
prop.wire(m.method)
m.reset_mock()
prop.setValue("after")
m.method.assert_called_with("after")

View File

@ -0,0 +1,51 @@
from unittest import TestCase
from unittest.mock import Mock
from owrx.property import PropertyLayer, PropertyFilter
class PropertyFilterTest(TestCase):
def testPassesProperty(self):
pm = PropertyLayer()
pm["testkey"] = "testvalue"
pf = PropertyFilter(pm, "testkey")
self.assertEqual(pf["testkey"], "testvalue")
def testMissesPropert(self):
pm = PropertyLayer()
pm["testkey"] = "testvalue"
pf = PropertyFilter(pm, "other_key")
self.assertFalse("testkey" in pf)
with self.assertRaises(KeyError):
x = pf["testkey"]
def testForwardsEvent(self):
pm = PropertyLayer()
pf = PropertyFilter(pm, "testkey")
mock = Mock()
pf.wire(mock.method)
pm["testkey"] = "testvalue"
mock.method.assert_called_once_with("testkey", "testvalue")
def testForwardsPropertyEvent(self):
pm = PropertyLayer()
pf = PropertyFilter(pm, "testkey")
mock = Mock()
pf.wireProperty("testkey", mock.method)
pm["testkey"] = "testvalue"
mock.method.assert_called_once_with("testvalue")
def testForwardsWrite(self):
pm = PropertyLayer()
pf = PropertyFilter(pm, "testkey")
pf["testkey"] = "testvalue"
self.assertTrue("testkey" in pm)
self.assertEqual(pm["testkey"], "testvalue")
def testOverwrite(self):
pm = PropertyLayer()
pm["testkey"] = "old value"
pf = PropertyFilter(pm, "testkey")
pf["testkey"] = "new value"
self.assertEqual(pm["testkey"], "new value")
self.assertEqual(pf["testkey"], "new value")

View File

@ -0,0 +1,52 @@
from owrx.property import PropertyLayer
from unittest import TestCase
from unittest.mock import Mock
class PropertyLayerTest(TestCase):
def testKeyIsset(self):
pm = PropertyLayer()
self.assertFalse("some_key" in pm)
def testKeyError(self):
pm = PropertyLayer()
with self.assertRaises(KeyError):
x = pm["some_key"]
def testSubscription(self):
pm = PropertyLayer()
pm["testkey"] = "before"
mock = Mock()
pm.wire(mock.method)
pm["testkey"] = "after"
mock.method.assert_called_once_with("testkey", "after")
def testUnsubscribe(self):
pm = PropertyLayer()
pm["testkey"] = "before"
mock = Mock()
sub = pm.wire(mock.method)
pm["testkey"] = "between"
mock.method.assert_called_once_with("testkey", "between")
mock.reset_mock()
pm.unwire(sub)
pm["testkey"] = "after"
mock.method.assert_not_called()
def testContains(self):
pm = PropertyLayer()
pm["testkey"] = "value"
self.assertTrue("testkey" in pm)
def testDoesNotContain(self):
pm = PropertyLayer()
self.assertFalse("testkey" in pm)
def testSubscribeBeforeSet(self):
pm = PropertyLayer()
mock = Mock()
pm.wireProperty("testkey", mock.method)
mock.method.assert_not_called()
pm["testkey"] = "newvalue"
mock.method.assert_called_once_with("newvalue")

View File

@ -1,20 +1,20 @@
from unittest import TestCase
from unittest.mock import Mock
from owrx.property import PropertyManager, PropertyLayers
from owrx.property import PropertyLayer, PropertyStack
class TestPropertyLayers(TestCase):
class PropertyStackTest(TestCase):
def testLayer(self):
om = PropertyLayers()
pm = PropertyManager()
om = PropertyStack()
pm = PropertyLayer()
pm["testkey"] = "testvalue"
om.addLayer(1, pm)
self.assertEqual(om["testkey"], "testvalue")
def testHighPriority(self):
om = PropertyLayers()
low_pm = PropertyManager()
high_pm = PropertyManager()
om = PropertyStack()
low_pm = PropertyLayer()
high_pm = PropertyLayer()
low_pm["testkey"] = "low value"
high_pm["testkey"] = "high value"
om.addLayer(1, low_pm)
@ -22,18 +22,18 @@ class TestPropertyLayers(TestCase):
self.assertEqual(om["testkey"], "high value")
def testPriorityFallback(self):
om = PropertyLayers()
low_pm = PropertyManager()
high_pm = PropertyManager()
om = PropertyStack()
low_pm = PropertyLayer()
high_pm = PropertyLayer()
low_pm["testkey"] = "low value"
om.addLayer(1, low_pm)
om.addLayer(0, high_pm)
self.assertEqual(om["testkey"], "low value")
def testLayerRemoval(self):
om = PropertyLayers()
low_pm = PropertyManager()
high_pm = PropertyManager()
om = PropertyStack()
low_pm = PropertyLayer()
high_pm = PropertyLayer()
low_pm["testkey"] = "low value"
high_pm["testkey"] = "high value"
om.addLayer(1, low_pm)