implement property carousel for profile switching
This commit is contained in:
parent
c7db144f7b
commit
6bd47cf914
@ -183,7 +183,7 @@ class PropertyFilter(PropertyManager):
|
|||||||
class PropertyDelegator(PropertyManager):
|
class PropertyDelegator(PropertyManager):
|
||||||
def __init__(self, pm: PropertyManager):
|
def __init__(self, pm: PropertyManager):
|
||||||
self.pm = pm
|
self.pm = pm
|
||||||
self.pm.wire(self._fireCallbacks)
|
self.subscription = self.pm.wire(self._fireCallbacks)
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
def __getitem__(self, item):
|
def __getitem__(self, item):
|
||||||
@ -340,3 +340,27 @@ class PropertyStack(PropertyManager):
|
|||||||
|
|
||||||
def keys(self):
|
def keys(self):
|
||||||
return set([key for l in self.layers for key in l["props"].keys()])
|
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)
|
||||||
|
@ -9,7 +9,7 @@ import signal
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from owrx.command import CommandMapper
|
from owrx.command import CommandMapper
|
||||||
from owrx.socket import getAvailablePort
|
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.property.filter import ByLambda
|
||||||
from owrx.form import Input, TextInput, NumberInput, CheckboxInput, ModesInput, ExponentialInput
|
from owrx.form import Input, TextInput, NumberInput, CheckboxInput, ModesInput, ExponentialInput
|
||||||
from owrx.form.converter import OptionalConverter
|
from owrx.form.converter import OptionalConverter
|
||||||
@ -67,7 +67,16 @@ class SdrSource(ABC):
|
|||||||
self.commandMapper = None
|
self.commandMapper = None
|
||||||
|
|
||||||
self.props = PropertyStack()
|
self.props = PropertyStack()
|
||||||
|
|
||||||
# layer 0 reserved for profile properties
|
# 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
|
# layer for runtime writeable properties
|
||||||
# these may be set during runtime, but should not be persisted to disk with the configuration
|
# 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:
|
if self.isAlwaysOn() and self.state is not SdrSourceState.DISABLED:
|
||||||
self.start()
|
self.start()
|
||||||
|
|
||||||
def _loadProfile(self, profile):
|
|
||||||
self.props.replaceLayer(0, PropertyFilter(profile, ByLambda(lambda x: x != "name")))
|
|
||||||
|
|
||||||
def validateProfiles(self):
|
def validateProfiles(self):
|
||||||
props = PropertyStack()
|
props = PropertyStack()
|
||||||
props.addLayer(1, self.props)
|
props.addLayer(1, self.props)
|
||||||
@ -154,20 +160,15 @@ class SdrSource(ABC):
|
|||||||
return [self.getCommandMapper().map(self.getCommandValues())]
|
return [self.getCommandMapper().map(self.getCommandValues())]
|
||||||
|
|
||||||
def activateProfile(self, profile_id=None):
|
def activateProfile(self, profile_id=None):
|
||||||
profiles = self.props["profiles"]
|
|
||||||
if profile_id is None:
|
if profile_id is None:
|
||||||
|
profiles = self.props["profiles"]
|
||||||
profile_id = list(profiles.keys())[0]
|
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))
|
logger.debug("activating profile {0}".format(profile_id))
|
||||||
self.props["profile_id"] = profile_id
|
try:
|
||||||
profile = profiles[profile_id]
|
self.profileCarousel.switch(profile_id)
|
||||||
self.profile_id = profile_id
|
self.profile_id = profile_id
|
||||||
|
except KeyError:
|
||||||
self._loadProfile(profile)
|
logger.warning("invalid profile %s for sdr %s. ignoring", profile_id, self.id)
|
||||||
|
|
||||||
def getId(self):
|
def getId(self):
|
||||||
return self.id
|
return self.id
|
||||||
|
78
test/property/test_property_carousel.py
Normal file
78
test/property/test_property_carousel.py
Normal 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")
|
Loading…
Reference in New Issue
Block a user