Source code for magicbot.magic_tunable

import functools
import inspect
import warnings
from typing import Callable, Generic, Optional, TypeVar, overload

from ntcore import NetworkTableInstance, Value
from ntcore.types import ValueT

T = TypeVar("T")
V = TypeVar("V", bound=ValueT)


[docs] class tunable(Generic[V]): """ This allows you to define simple properties that allow you to easily communicate with other programs via NetworkTables. The following example will define a NetworkTable variable at ``/components/my_component/foo``:: class MyRobot(magicbot.MagicRobot): my_component: MyComponent ... from magicbot import tunable class MyComponent: # define the tunable property foo = tunable(True) def execute(self): # set the variable self.foo = True # get the variable foo = self.foo The key of the NetworkTables variable will vary based on what kind of object the decorated method belongs to: * A component: ``/components/COMPONENTNAME/VARNAME`` * An autonomous mode: ``/autonomous/MODENAME/VARNAME`` * Your main robot class: ``/robot/VARNAME`` .. note:: When executing unit tests on objects that create tunables, you will want to use setup_tunables to set the object up. In normal usage, MagicRobot does this for you, so you don't have to do anything special. """ # the way this works is we use a special class to indicate that it # is a tunable, and MagicRobot adds _ntattr and _global_table variables # to the class property # The tricky bit is that you need to do late binding on these, because # the networktables key is not known when the object is created. Instead, # the name of the key is related to the name of the variable name in the # robot class __slots__ = ( "_ntdefault", "_ntsubtable", "_ntwritedefault", # "__doc__", "_mkv", "_nt", ) def __init__( self, default: V, *, writeDefault: bool = True, subtable: Optional[str] = None, doc=None, ) -> None: if doc is not None: warnings.warn("tunable no longer uses the doc argument", stacklevel=2) self._ntdefault = default self._ntsubtable = subtable self._ntwritedefault = writeDefault d = Value.makeValue(default) self._mkv = Value.getFactoryByType(d.type()) # self.__doc__ = doc @overload def __get__(self, instance: None, owner=None) -> "tunable[V]": ... @overload def __get__(self, instance, owner=None) -> V: ... def __get__(self, instance, owner=None): if instance is not None: return instance._tunables[self].value return self def __set__(self, instance, value: V) -> None: instance._tunables[self].setValue(self._mkv(value))
[docs] def setup_tunables(component, cname: str, prefix: Optional[str] = "components") -> None: """ Connects the tunables on an object to NetworkTables. :param component: Component object :param cname: Name of component :param prefix: Prefix to use, or no prefix if None .. note:: This is not needed in normal use, only useful for testing """ cls = component.__class__ if prefix is None: prefix = "/%s" % cname else: prefix = "/%s/%s" % (prefix, cname) NetworkTables = NetworkTableInstance.getDefault() tunables = {} for n in dir(cls): if n.startswith("_"): continue prop = getattr(cls, n) if not isinstance(prop, tunable): continue if prop._ntsubtable: key = "%s/%s/%s" % (prefix, prop._ntsubtable, n) else: key = "%s/%s" % (prefix, n) ntvalue = NetworkTables.getEntry(key) if prop._ntwritedefault: ntvalue.setValue(prop._ntdefault) else: ntvalue.setDefaultValue(prop._ntdefault) tunables[prop] = ntvalue component._tunables = tunables
@overload def feedback(f: Callable[[T], V]) -> Callable[[T], V]: ... @overload def feedback(*, key: str) -> Callable[[Callable[[T], V]], Callable[[T], V]]: ...
[docs] def feedback(f=None, *, key: Optional[str] = None) -> Callable: """ This decorator allows you to create NetworkTables values that are automatically updated with the return value of a method. ``key`` is an optional parameter, and if it is not supplied, the key will default to the method name with a leading ``get_`` removed. If the method does not start with ``get_``, the key will be the full name of the method. The key of the NetworkTables value will vary based on what kind of object the decorated method belongs to: * A component: ``/components/COMPONENTNAME/VARNAME`` * Your main robot class: ``/robot/VARNAME`` The NetworkTables value will be auto-updated in all modes. .. warning:: The function should only act as a getter, and must not take any arguments (other than self). Example:: from magicbot import feedback class MyComponent: navx: ... @feedback def get_angle(self): return self.navx.getYaw() class MyRobot(magicbot.MagicRobot): my_component: MyComponent ... In this example, the NetworkTable key is stored at ``/components/my_component/angle``. .. seealso:: :class:`~wpilib.LiveWindow` may suit your needs, especially if you wish to monitor WPILib objects. .. versionadded:: 2018.1.0 """ if f is None: return functools.partial(feedback, key=key) if not callable(f): raise TypeError(f"Illegal use of feedback decorator on non-callable {f!r}") sig = inspect.signature(f) name = f.__name__ if len(sig.parameters) != 1: raise ValueError( f"{name} may not take arguments other than 'self' (must be a simple getter method)" ) # Set attributes to be checked during injection f._magic_feedback = True f._magic_feedback_key = key return f
[docs] def collect_feedbacks(component, cname: str, prefix: Optional[str] = "components"): """ Finds all methods decorated with :func:`feedback` on an object and returns a list of 2-tuples (method, NetworkTables entry). .. note:: This isn't useful for normal use. """ if prefix is None: prefix = "/%s" % cname else: prefix = "/%s/%s" % (prefix, cname) nt = NetworkTableInstance.getDefault().getTable(prefix) feedbacks = [] for name, method in inspect.getmembers(component, inspect.ismethod): if getattr(method, "_magic_feedback", False): key = method._magic_feedback_key if key is None: if name.startswith("get_"): key = name[4:] else: key = name entry = nt.getEntry(key) feedbacks.append((method, entry)) return feedbacks