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