implement property carousel for profile switching

This commit is contained in:
Jakob Ketterl 2021-03-01 00:26:56 +01:00
parent c7db144f7b
commit 6bd47cf914
3 changed files with 119 additions and 16 deletions

View File

@ -183,7 +183,7 @@ class PropertyFilter(PropertyManager):
class PropertyDelegator(PropertyManager):
def __init__(self, pm: PropertyManager):
self.pm = pm
self.pm.wire(self._fireCallbacks)
self.subscription = self.pm.wire(self._fireCallbacks)
super().__init__()
def __getitem__(self, item):
@ -340,3 +340,27 @@ class PropertyStack(PropertyManager):
def keys(self):
return set([key for l in self.layers for key in l["props"].keys()])
class PropertyCarousel(PropertyDelegator):
def __init__(self):
# start with an empty dummy layer
super().__init__(PropertyLayer())
self.layers = {}
def addLayer(self, key, value):
self.layers[key] = value
def switch(self, key):
before = self.pm
self.subscription.cancel()
self.pm = self.layers[key]
self.subscription = self.pm.wire(self._fireCallbacks)
changes = {}
for key in set(list(before.keys()) + list(self.keys())):
if key not in self:
changes[key] = PropertyDeleted
else:
if key not in before or before[key] != self[key]:
changes[key] = self[key]
self._fireCallbacks(changes)

View File

@ -9,7 +9,7 @@ import signal
from abc import ABC, abstractmethod
from owrx.command import CommandMapper
from owrx.socket import getAvailablePort
from owrx.property import PropertyStack, PropertyLayer, PropertyFilter
from owrx.property import PropertyStack, PropertyLayer, PropertyFilter, PropertyCarousel
from owrx.property.filter import ByLambda
from owrx.form import Input, TextInput, NumberInput, CheckboxInput, ModesInput, ExponentialInput
from owrx.form.converter import OptionalConverter
@ -67,7 +67,16 @@ class SdrSource(ABC):
self.commandMapper = None
self.props = PropertyStack()
# layer 0 reserved for profile properties
self.profileCarousel = PropertyCarousel()
if "profiles" in props:
for profile_id, profile in props["profiles"].items():
profile_stack = PropertyStack()
profile_stack.addLayer(0, PropertyLayer(profile_id=profile_id).readonly())
profile_stack.addLayer(1, profile)
self.profileCarousel.addLayer(profile_id, profile_stack)
self.props.addLayer(0, PropertyFilter(self.profileCarousel, ByLambda(lambda x: x != "name")))
# layer for runtime writeable properties
# these may be set during runtime, but should not be persisted to disk with the configuration
@ -105,9 +114,6 @@ class SdrSource(ABC):
if self.isAlwaysOn() and self.state is not SdrSourceState.DISABLED:
self.start()
def _loadProfile(self, profile):
self.props.replaceLayer(0, PropertyFilter(profile, ByLambda(lambda x: x != "name")))
def validateProfiles(self):
props = PropertyStack()
props.addLayer(1, self.props)
@ -154,20 +160,15 @@ class SdrSource(ABC):
return [self.getCommandMapper().map(self.getCommandValues())]
def activateProfile(self, profile_id=None):
profiles = self.props["profiles"]
if profile_id is None:
profiles = self.props["profiles"]
profile_id = list(profiles.keys())[0]
if profile_id not in profiles:
logger.warning("invalid profile %s for sdr %s. ignoring", profile_id, self.id)
return
if profile_id == self.profile_id:
return
logger.debug("activating profile {0}".format(profile_id))
self.props["profile_id"] = profile_id
profile = profiles[profile_id]
self.profile_id = profile_id
self._loadProfile(profile)
try:
self.profileCarousel.switch(profile_id)
self.profile_id = profile_id
except KeyError:
logger.warning("invalid profile %s for sdr %s. ignoring", profile_id, self.id)
def getId(self):
return self.id

View File

@ -0,0 +1,78 @@
from unittest import TestCase
from unittest.mock import Mock
from owrx.property import PropertyCarousel, PropertyLayer, PropertyDeleted
class PropertyCarouselTest(TestCase):
def testInitiallyEmpty(self):
pc = PropertyCarousel()
with self.assertRaises(KeyError):
x = pc["testkey"]
def testPropertyAccess(self):
pc = PropertyCarousel()
pl = PropertyLayer(testkey="testvalue")
pc.addLayer("test", pl)
pc.switch("test")
self.assertEqual(pc["testkey"], "testvalue")
def testWriteAccess(self):
pc = PropertyCarousel()
pl = PropertyLayer(testkey="old_value")
pc.addLayer("test", pl)
pc.switch("test")
pc["testkey"] = "new_value"
self.assertEqual(pc["testkey"], "new_value")
self.assertEqual(pl["testkey"], "new_value")
def testForwardsEvents(self):
pc = PropertyCarousel()
pl = PropertyLayer(testkey="old_value")
pc.addLayer("test", pl)
pc.switch("test")
mock = Mock()
pc.wire(mock.method)
pc["testkey"] = "new_value"
mock.method.assert_called_once_with({"testkey": "new_value"})
def testStopsForwardingAfterSwitch(self):
pc = PropertyCarousel()
pl_x = PropertyLayer(testkey="old_value")
pc.addLayer("x", pl_x)
pl_y = PropertyLayer(testkey="new_value")
pc.addLayer("y", pl_y)
pc.switch("x")
pc.switch("y")
mock = Mock()
pc.wire(mock.method)
pl_x["testkey"] = "new_value"
mock.method.assert_not_called()
def testEventsOnSwitch(self):
pc = PropertyCarousel()
pl_x = PropertyLayer(old_key="old_value")
pc.addLayer("x", pl_x)
pl_y = PropertyLayer(new_key="new_value")
pc.addLayer("y", pl_y)
pc.switch("x")
mock = Mock()
pc.wire(mock.method)
pc.switch("y")
mock.method.assert_called_once_with({"old_key": PropertyDeleted, "new_key": "new_value"})
def testNoEventsIfKeysDontChange(self):
pc = PropertyCarousel()
pl_x = PropertyLayer(testkey="same_value")
pc.addLayer("x", pl_x)
pl_y = PropertyLayer(testkey="same_value")
pc.addLayer("y", pl_y)
pc.switch("x")
mock = Mock()
pc.wire(mock.method)
pc.switch("y")
mock.method.assert_not_called()
def testKeyErrorOnInvalidSwitch(self):
pc = PropertyCarousel()
with self.assertRaises(KeyError):
pc.switch("doesntmatter")