├── networktables ├── py.typed ├── entry.py ├── networktable.py ├── __init__.pyi ├── __init__.py └── util.py ├── _pynetworktables ├── py.typed ├── _impl │ ├── support │ │ ├── __init__.py │ │ ├── lists.py │ │ ├── uidvector.py │ │ ├── _impl.py │ │ ├── leb128.py │ │ ├── safe_thread.py │ │ └── _impl_debug.py │ ├── tcpsockets │ │ ├── __init__.py │ │ ├── tcp_stream.py │ │ ├── tcp_connector.py │ │ └── tcp_acceptor.py │ ├── __init__.py │ ├── structs.py │ ├── connection_notifier.py │ ├── constants.py │ ├── rpc_server.py │ ├── storage_save.py │ ├── value.py │ ├── ds_client.py │ ├── entry_notifier.py │ ├── storage_load.py │ ├── wire.py │ ├── message.py │ ├── api.py │ └── callback_manager.py └── __init__.py ├── MANIFEST.in ├── docs ├── requirements.txt ├── api.rst ├── examples.rst ├── index.rst ├── gensidebar.py ├── conf.py ├── Makefile └── make.bat ├── tests ├── requirements.txt ├── run_tests.sh ├── run_tests.py ├── test_types.py ├── test_entry.py ├── test_seqnum.py ├── test_uleb128.py ├── test_conn_listener.py ├── test_util.py ├── test_value.py ├── test_entry_listener.py ├── test_network_table.py ├── test_api.py ├── conftest.py ├── test_wire.py ├── test_network_table_listener.py └── test_entry_notifier.py ├── setup.cfg ├── .gitignore ├── .gittrack ├── .gittrackexclude ├── samples ├── auto_listener.py ├── nt_driverstation.py ├── nt_robot.py ├── global_listener.py ├── listen_chooser.py ├── listener.py ├── ntproperty_client.py ├── json_logger │ ├── plot.py │ ├── robot.py │ └── logger.py └── benchmark.py ├── LICENSE.txt ├── .github └── workflows │ └── dist.yml ├── setup.py ├── README.rst └── tools └── fuzzer.py /networktables/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_pynetworktables/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include samples/*.py 2 | -------------------------------------------------------------------------------- /_pynetworktables/_impl/support/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_pynetworktables/_impl/tcpsockets/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx-autodoc-typehints 2 | -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | pytest>=3.0 2 | coverage 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | # Include the license file in wheels. 3 | license_file = LICENSE.txt 4 | -------------------------------------------------------------------------------- /_pynetworktables/_impl/__init__.py: -------------------------------------------------------------------------------- 1 | try: 2 | from .version import __version__ 3 | except ImportError: 4 | __version__ = "master" 5 | -------------------------------------------------------------------------------- /tests/run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | cd `dirname $0` 5 | 6 | PYTHONPATH=.. python -m coverage run --source networktables,_pynetworktables -m pytest "$@" 7 | python -m coverage report -m 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | 4 | docs/_build 5 | docs/_sidebar.rst.inc 6 | 7 | _pynetworktables/_impl/version.py 8 | 9 | *.egg-info 10 | *.py[cod] 11 | __pycache__ 12 | .coverage 13 | .pydevproject 14 | .project 15 | -------------------------------------------------------------------------------- /.gittrack: -------------------------------------------------------------------------------- 1 | [git-source-track] 2 | upstream_commit = 60c2f5905191c25c3e4ff2fc735cec2ca5f012bf 3 | validation_root = ntcore 4 | exclude_commits_file = .gittrackexclude 5 | upstream_root = ../allwpilib/ntcore/src/main/native 6 | 7 | -------------------------------------------------------------------------------- /networktables/entry.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | from . import NetworkTableEntry # noqa 4 | 5 | warnings.warn( 6 | "networktables.entry is deprecated, import networktables.NetworkTableEntry directly", 7 | DeprecationWarning, 8 | stacklevel=2, 9 | ) 10 | -------------------------------------------------------------------------------- /networktables/networktable.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | from . import NetworkTable # noqa 4 | 5 | warnings.warn( 6 | "networktables.networktable is deprecated, import networktables.NetworkTable directly", 7 | DeprecationWarning, 8 | stacklevel=2, 9 | ) 10 | -------------------------------------------------------------------------------- /_pynetworktables/_impl/support/lists.py: -------------------------------------------------------------------------------- 1 | # notrack 2 | 3 | from collections import namedtuple 4 | 5 | Pair = namedtuple("Pair", ["first", "second"]) 6 | 7 | 8 | def ensure_id_exists(lst, msg_id, default=None): 9 | if msg_id >= len(lst): 10 | lst += [default] * (msg_id - len(lst) + 1) 11 | -------------------------------------------------------------------------------- /tests/run_tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | from os.path import abspath, dirname 5 | import sys 6 | import subprocess 7 | 8 | if __name__ == "__main__": 9 | 10 | root = abspath(dirname(__file__)) 11 | os.chdir(root) 12 | 13 | subprocess.check_call([sys.executable, "-m", "pytest", "-vv"]) 14 | -------------------------------------------------------------------------------- /_pynetworktables/_impl/support/uidvector.py: -------------------------------------------------------------------------------- 1 | # novalidate 2 | 3 | import threading 4 | 5 | 6 | class UidVector(dict): 7 | def __init__(self): 8 | self.idx = 0 9 | self.lock = threading.Lock() 10 | 11 | def add(self, item): 12 | """Only use this method to add to the UidVector""" 13 | with self.lock: 14 | idx = self.idx 15 | self.idx += 1 16 | 17 | self[idx] = item 18 | return idx 19 | -------------------------------------------------------------------------------- /networktables/__init__.pyi: -------------------------------------------------------------------------------- 1 | from _pynetworktables import ( 2 | NetworkTablesInstance as NetworkTablesInstance, 3 | NetworkTables as NetworkTables, 4 | NetworkTable as NetworkTable, 5 | NetworkTableEntry as NetworkTableEntry, 6 | Value as Value, 7 | __version__ as __version__, 8 | ) 9 | 10 | nt_backend: str = ... 11 | 12 | __all__ = ( 13 | "NetworkTablesInstance", 14 | "NetworkTables", 15 | "NetworkTable", 16 | "NetworkTableEntry", 17 | "Value", 18 | ) 19 | -------------------------------------------------------------------------------- /_pynetworktables/__init__.py: -------------------------------------------------------------------------------- 1 | from ._impl import __version__ 2 | 3 | from .entry import NetworkTableEntry 4 | from .instance import NetworkTablesInstance 5 | from .table import NetworkTable 6 | from ._impl.value import Value 7 | 8 | #: Alias of NetworkTablesInstance.getDefault(), the "default" instance 9 | NetworkTables = NetworkTablesInstance.getDefault() 10 | 11 | __all__ = ( 12 | "NetworkTablesInstance", 13 | "NetworkTables", 14 | "NetworkTable", 15 | "NetworkTableEntry", 16 | "Value", 17 | ) 18 | -------------------------------------------------------------------------------- /.gittrackexclude: -------------------------------------------------------------------------------- 1 | 05ca76ea990f5cca713164801960ddc410d4cba7 # clang-format 2 | aa2de65bad663d3d72e08fddb1c166e01cefa3e8 # llvm::Twine 3 | 3438a173410376277ee85e7befa068be28cf8dcd # wpi::mutex (priority-aware mutex) 4 | 19f7a5f1082148b4241333deb6ffe266a806a775 # wpiformat 5 | 4376c94dc19920eb5a6dd1ca6f8b3a948bd28681 # bump copyright 6 | 7210a8fd289cf74ce5be34977355f3d3948083c9 # prepare allwpilib merge 7 | f84018af5f71d02e630b2bdad8aa61c08f6f1292 # move llvm to wpi 8 | 7a34f5d17d174beec2fd82dd6a05223e80091a8b # checked malloc 9 | 6729a7d6b1dfd9d23b58f51d3e7e76e5d0bf42aa # wpiformat on merged repo 10 | -------------------------------------------------------------------------------- /tests/test_types.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from _pynetworktables.entry import NetworkTableEntry 3 | 4 | 5 | @pytest.mark.parametrize( 6 | "data, expected_result", 7 | [ 8 | ("a", True), 9 | (1, True), 10 | (1.0, True), 11 | (1.0j, False), 12 | (False, True), 13 | (b"1", True), 14 | (bytearray(), True), 15 | ((), ValueError), 16 | ([], ValueError), 17 | ], 18 | ) 19 | def test_isValidType(data, expected_result): 20 | if isinstance(expected_result, bool): 21 | assert NetworkTableEntry.isValidDataType(data) == expected_result 22 | else: 23 | with pytest.raises(expected_result): 24 | NetworkTableEntry.isValidDataType(data) 25 | -------------------------------------------------------------------------------- /networktables/__init__.py: -------------------------------------------------------------------------------- 1 | # prefer pyntcore if installed 2 | try: 3 | from _pyntcore import ( 4 | NetworkTablesInstance, 5 | NetworkTables, 6 | NetworkTable, 7 | NetworkTableEntry, 8 | Value, 9 | __version__, 10 | ) 11 | 12 | nt_backend = "pyntcore" 13 | except ImportError as e: 14 | from _pynetworktables import ( 15 | NetworkTablesInstance, 16 | NetworkTables, 17 | NetworkTable, 18 | NetworkTableEntry, 19 | Value, 20 | __version__, 21 | ) 22 | 23 | nt_backend = "pynetworktables" 24 | 25 | 26 | __all__ = ( 27 | "NetworkTablesInstance", 28 | "NetworkTables", 29 | "NetworkTable", 30 | "NetworkTableEntry", 31 | "Value", 32 | ) 33 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _pynetworktables_api: 3 | 4 | API Reference 5 | ============= 6 | 7 | .. toctree:: 8 | 9 | examples 10 | 11 | NetworkTables API 12 | ----------------- 13 | 14 | .. autoclass:: networktables.NetworkTablesInstance 15 | :members: 16 | :undoc-members: 17 | :exclude-members: globalDeleteAll, addGlobalListener, addGlobalListenerEx, removeGlobalListener 18 | 19 | NetworkTable Objects 20 | -------------------- 21 | 22 | .. autoclass:: networktables.NetworkTable 23 | :members: 24 | :undoc-members: 25 | :exclude-members: addTableListener, addTableListenerEx, removeTableListener 26 | 27 | .. autoclass:: networktables.NetworkTableEntry 28 | :members: 29 | :undoc-members: 30 | 31 | Utilities 32 | --------- 33 | 34 | .. automodule:: networktables.util 35 | :members: 36 | :undoc-members: 37 | -------------------------------------------------------------------------------- /_pynetworktables/_impl/support/_impl.py: -------------------------------------------------------------------------------- 1 | # notrack 2 | 3 | __all__ = ["create_rlock", "sock_makefile"] 4 | 5 | import socket 6 | import threading 7 | 8 | 9 | def create_rlock(name): 10 | return threading.RLock() 11 | 12 | 13 | def sock_makefile(s, mode): 14 | return s.makefile(mode) 15 | 16 | 17 | def sock_create_connection(address): 18 | return socket.create_connection(address) 19 | 20 | 21 | # Call this before creating any NetworkTable objects 22 | def enable_lock_debugging(sock_block_period=None): 23 | 24 | from . import _impl_debug 25 | 26 | _impl_debug.sock_block_period = sock_block_period 27 | 28 | g = globals() 29 | g["create_rlock"] = _impl_debug.create_tracked_rlock 30 | g["sock_makefile"] = _impl_debug.blocking_sock_makefile 31 | g["sock_create_connection"] = _impl_debug.blocking_sock_create_connection 32 | -------------------------------------------------------------------------------- /tests/test_entry.py: -------------------------------------------------------------------------------- 1 | # 2 | # Ensure that the NetworkTableEntry objects work 3 | # 4 | 5 | 6 | def test_entry_value(nt): 7 | e = nt.getEntry("/k1") 8 | assert e.getString(None) is None 9 | e.setString("value") 10 | assert e.getString(None) == "value" 11 | e.delete() 12 | assert e.getString(None) is None 13 | e.setString("value") 14 | assert e.getString(None) == "value" 15 | 16 | 17 | def test_entry_persistence(nt): 18 | e = nt.getEntry("/k2") 19 | 20 | for _ in range(2): 21 | 22 | assert not e.isPersistent() 23 | # persistent flag cannot be set unless the entry has a value 24 | e.setString("value") 25 | 26 | assert not e.isPersistent() 27 | e.setPersistent() 28 | assert e.isPersistent() 29 | e.clearPersistent() 30 | assert not e.isPersistent() 31 | 32 | e.delete() 33 | -------------------------------------------------------------------------------- /samples/auto_listener.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # This is a NetworkTables client (eg, the DriverStation/coprocessor side). 4 | # You need to tell it the IP address of the NetworkTables server (the 5 | # robot or simulator). 6 | # 7 | # When running, this will create an automatically updated value, and print 8 | # out the value. 9 | # 10 | 11 | import sys 12 | import time 13 | from networktables import NetworkTables 14 | 15 | # To see messages from networktables, you must setup logging 16 | import logging 17 | 18 | logging.basicConfig(level=logging.DEBUG) 19 | 20 | if len(sys.argv) != 2: 21 | print("Error: specify an IP to connect to!") 22 | exit(0) 23 | 24 | ip = sys.argv[1] 25 | 26 | NetworkTables.initialize(server=ip) 27 | 28 | sd = NetworkTables.getTable("SmartDashboard") 29 | auto_value = sd.getAutoUpdateValue("robotTime", 0) 30 | 31 | while True: 32 | print("robotTime:", auto_value.value) 33 | time.sleep(1) 34 | -------------------------------------------------------------------------------- /samples/nt_driverstation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # This is a NetworkTables client (eg, the DriverStation/coprocessor side). 4 | # You need to tell it the IP address of the NetworkTables server (the 5 | # robot or simulator). 6 | # 7 | # When running, this will continue incrementing the value 'dsTime', and the 8 | # value should be visible to other networktables clients and the robot. 9 | # 10 | 11 | import sys 12 | import time 13 | from networktables import NetworkTables 14 | 15 | # To see messages from networktables, you must setup logging 16 | import logging 17 | 18 | logging.basicConfig(level=logging.DEBUG) 19 | 20 | if len(sys.argv) != 2: 21 | print("Error: specify an IP to connect to!") 22 | exit(0) 23 | 24 | ip = sys.argv[1] 25 | 26 | NetworkTables.initialize(server=ip) 27 | 28 | sd = NetworkTables.getTable("SmartDashboard") 29 | 30 | i = 0 31 | while True: 32 | print("robotTime:", sd.getNumber("robotTime", -1)) 33 | 34 | sd.putNumber("dsTime", i) 35 | time.sleep(1) 36 | i += 1 37 | -------------------------------------------------------------------------------- /samples/nt_robot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # This is a NetworkTables server (eg, the robot or simulator side). 4 | # 5 | # On a real robot, you probably would create an instance of the 6 | # wpilib.SmartDashboard object and use that instead -- but it's really 7 | # just a passthru to the underlying NetworkTable object. 8 | # 9 | # When running, this will continue incrementing the value 'robotTime', 10 | # and the value should be visible to networktables clients such as 11 | # SmartDashboard. To view using the SmartDashboard, you can launch it 12 | # like so: 13 | # 14 | # SmartDashboard.jar ip 127.0.0.1 15 | # 16 | 17 | import time 18 | from networktables import NetworkTables 19 | 20 | # To see messages from networktables, you must setup logging 21 | import logging 22 | 23 | logging.basicConfig(level=logging.DEBUG) 24 | 25 | NetworkTables.initialize() 26 | sd = NetworkTables.getTable("SmartDashboard") 27 | 28 | i = 0 29 | while True: 30 | print("dsTime:", sd.getNumber("dsTime", -1)) 31 | 32 | sd.putNumber("robotTime", i) 33 | time.sleep(1) 34 | i += 1 35 | -------------------------------------------------------------------------------- /samples/global_listener.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # This is a NetworkTables client (eg, the DriverStation/coprocessor side). 4 | # You need to tell it the IP address of the NetworkTables server (the 5 | # robot or simulator). 6 | # 7 | # This shows how to use a listener to listen for all changes in NetworkTables 8 | # values, which prints out all changes. Note that the keys are full paths, and 9 | # not just individual key values. 10 | # 11 | 12 | import sys 13 | import time 14 | from networktables import NetworkTables 15 | 16 | # To see messages from networktables, you must setup logging 17 | import logging 18 | 19 | logging.basicConfig(level=logging.DEBUG) 20 | 21 | if len(sys.argv) != 2: 22 | print("Error: specify an IP to connect to!") 23 | exit(0) 24 | 25 | ip = sys.argv[1] 26 | 27 | NetworkTables.initialize(server=ip) 28 | 29 | 30 | def valueChanged(key, value, isNew): 31 | print("valueChanged: key: '%s'; value: %s; isNew: %s" % (key, value, isNew)) 32 | 33 | 34 | NetworkTables.addEntryListener(valueChanged) 35 | 36 | while True: 37 | time.sleep(1) 38 | -------------------------------------------------------------------------------- /_pynetworktables/_impl/support/leb128.py: -------------------------------------------------------------------------------- 1 | # novalidate 2 | """ 3 | Operations related to LEB128 encoding/decoding 4 | 5 | The algorithm is taken from Appendix C of the DWARF 3 spec. For information 6 | on the encodings refer to section "7.6 - Variable Length Data" 7 | 8 | """ 9 | 10 | import sys 11 | 12 | 13 | def size_uleb128(value): 14 | count = 0 15 | while True: 16 | value >>= 7 17 | count += 1 18 | if value == 0: 19 | break 20 | return count 21 | 22 | 23 | def encode_uleb128(value): 24 | out = bytearray() 25 | while True: 26 | byte = value & 0x7F 27 | value >>= 7 28 | if value != 0: 29 | byte = byte | 0x80 30 | out.append(byte) 31 | if value == 0: 32 | break 33 | return out 34 | 35 | 36 | def read_uleb128(rstream): 37 | result = 0 38 | shift = 0 39 | while True: 40 | b = rstream.read(1)[0] 41 | result |= (b & 0x7F) << shift 42 | shift += 7 43 | if (b & 0x80) == 0: 44 | break 45 | return result 46 | -------------------------------------------------------------------------------- /samples/listen_chooser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # This is a NetworkTables client (eg, the DriverStation/coprocessor side). 4 | # You need to tell it the IP address of the NetworkTables server (the 5 | # robot or simulator). 6 | # 7 | # This shows how to use a listener to listen for changes to a SendableChooser 8 | # object. 9 | # 10 | 11 | from __future__ import print_function 12 | 13 | import sys 14 | import time 15 | from networktables import NetworkTables 16 | from networktables.util import ChooserControl 17 | 18 | # To see messages from networktables, you must setup logging 19 | import logging 20 | 21 | logging.basicConfig(level=logging.DEBUG) 22 | 23 | if len(sys.argv) != 2: 24 | print("Error: specify an IP to connect to!") 25 | exit(0) 26 | 27 | ip = sys.argv[1] 28 | 29 | NetworkTables.initialize(server=ip) 30 | 31 | 32 | def on_choices(value): 33 | print("OnChoices", value) 34 | 35 | 36 | def on_selected(value): 37 | print("OnSelected", value) 38 | 39 | 40 | cc = ChooserControl("Autonomous Mode", on_choices, on_selected) 41 | 42 | while True: 43 | time.sleep(1) 44 | -------------------------------------------------------------------------------- /_pynetworktables/_impl/structs.py: -------------------------------------------------------------------------------- 1 | # novalidate 2 | # fmt: off 3 | 4 | from collections import namedtuple 5 | 6 | #: NetworkTables Entry Information 7 | EntryInfo = namedtuple('EntryInfo', [ 8 | # Entry name 9 | 'name', 10 | 11 | # Entry type 12 | 'type', 13 | 14 | # Entry flags 15 | 'flags', 16 | 17 | # Timestamp of last change to entry (type or value). 18 | #'last_change', 19 | ]) 20 | 21 | 22 | #: NetworkTables Connection Information 23 | ConnectionInfo = namedtuple('ConnectionInfo', [ 24 | 'remote_id', 25 | 'remote_ip', 26 | 'remote_port', 27 | 'last_update', 28 | 'protocol_version', 29 | ]) 30 | 31 | 32 | #: NetworkTables RPC Parameter Definition 33 | RpcParamDef = namedtuple('RpcParamDef', [ 34 | 'name', 35 | 'def_value', 36 | ]) 37 | 38 | #: NetworkTables RPC Result Definition 39 | RpcResultDef = namedtuple('RpcResultDef', [ 40 | 'name', 41 | 'type', 42 | ]) 43 | 44 | #: NetworkTables RPC Definition 45 | RpcDefinition = namedtuple('RpcDefinition', [ 46 | 'version', 47 | 'name', 48 | 'params', 49 | 'results', 50 | ]) 51 | 52 | 53 | #: NetworkTables RPC Call Data 54 | RpcCallInfo = namedtuple('RpcCallInfo', [ 55 | 'rpc_id', 56 | 'call_uid', 57 | 'name', 58 | 'params', 59 | ]) 60 | -------------------------------------------------------------------------------- /samples/listener.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # This is a NetworkTables client (eg, the DriverStation/coprocessor side). 4 | # You need to tell it the IP address of the NetworkTables server (the 5 | # robot or simulator). 6 | # 7 | # This shows how to use a listener to listen for changes in NetworkTables 8 | # values. This will print out any changes detected on the SmartDashboard 9 | # table. 10 | # 11 | 12 | import sys 13 | import time 14 | from networktables import NetworkTables 15 | 16 | # To see messages from networktables, you must setup logging 17 | import logging 18 | 19 | logging.basicConfig(level=logging.DEBUG) 20 | 21 | if len(sys.argv) != 2: 22 | print("Error: specify an IP to connect to!") 23 | exit(0) 24 | 25 | ip = sys.argv[1] 26 | 27 | NetworkTables.initialize(server=ip) 28 | 29 | 30 | def valueChanged(table, key, value, isNew): 31 | print("valueChanged: key: '%s'; value: %s; isNew: %s" % (key, value, isNew)) 32 | 33 | 34 | def connectionListener(connected, info): 35 | print(info, "; Connected=%s" % connected) 36 | 37 | 38 | NetworkTables.addConnectionListener(connectionListener, immediateNotify=True) 39 | 40 | sd = NetworkTables.getTable("SmartDashboard") 41 | sd.addEntryListener(valueChanged) 42 | 43 | while True: 44 | time.sleep(1) 45 | -------------------------------------------------------------------------------- /docs/examples.rst: -------------------------------------------------------------------------------- 1 | .. _pynetworktables_examples: 2 | 3 | Examples 4 | ======== 5 | 6 | These are the simple examples that are included with pynetworktables. 7 | 8 | Robot Example 9 | ------------- 10 | 11 | .. literalinclude:: ../samples/nt_robot.py 12 | 13 | Driver Station Example 14 | ---------------------- 15 | 16 | .. literalinclude:: ../samples/nt_driverstation.py 17 | 18 | Listener Example 19 | ---------------- 20 | 21 | .. literalinclude:: ../samples/listener.py 22 | 23 | Listen Chooser Example 24 | ---------------------- 25 | 26 | .. literalinclude:: ../samples/listen_chooser.py 27 | 28 | Auto Listener Example 29 | --------------------- 30 | 31 | .. literalinclude:: ../samples/auto_listener.py 32 | 33 | Global Listener Example 34 | ----------------------- 35 | 36 | .. literalinclude:: ../samples/global_listener.py 37 | 38 | ntproperty Example 39 | ------------------ 40 | 41 | .. literalinclude:: ../samples/ntproperty_client.py 42 | 43 | json_logger Example 44 | ------------------- 45 | 46 | This is a more complex example which can be used to log data from your robot 47 | into a JSON file. There is a corresponding 48 | 49 | As this example is a bit larger than the others, see the 'samples/json_logger' 50 | directory of the pynetworktables repository on github. It also includes a script 51 | that you can modify to plot the data. 52 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | pynetworktables documentation 2 | ============================= 3 | 4 | This is a pure python implementation of the NetworkTables protocol, derived 5 | from the wpilib ntcore C++ implementation. In FRC, the NetworkTables protocol 6 | is used to pass non-Driver Station data to and from the robot across the network. 7 | 8 | Don't understand this NetworkTables thing? Check out our :ref:`basic overview of 9 | NetworkTables `. 10 | 11 | This implementation is intended to be compatible with python 2.7 and python 3.3+. 12 | All commits to the repository are automatically tested on all supported python 13 | versions using Travis-CI. 14 | 15 | .. note:: NetworkTables is a protocol used for robot communication in the 16 | FIRST Robotics Competition, and can be used to talk to 17 | SmartDashboard/SFX. It does not have any security, and should never 18 | be used on untrusted networks. 19 | 20 | .. important:: pynetworktables implements the NetworkTables 3 protocol, which is deprecated. 21 | It is not compatible with the 2027 control system. Use 22 | `pyntcore `_ 23 | instead. 24 | 25 | .. include:: _sidebar.rst.inc 26 | 27 | Indices and tables 28 | ================== 29 | 30 | * :ref:`genindex` 31 | * :ref:`modindex` 32 | * :ref:`search` 33 | -------------------------------------------------------------------------------- /samples/ntproperty_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # This is a NetworkTables client (eg, the DriverStation/coprocessor side). 4 | # You need to tell it the IP address of the NetworkTables server (the 5 | # robot or simulator). 6 | # 7 | # When running, this will continue incrementing the value 'dsTime', and the 8 | # value should be visible to other networktables clients and the robot. 9 | # 10 | 11 | import sys 12 | import time 13 | from networktables import NetworkTables 14 | from networktables.util import ntproperty 15 | 16 | # To see messages from networktables, you must setup logging 17 | import logging 18 | 19 | logging.basicConfig(level=logging.DEBUG) 20 | 21 | if len(sys.argv) != 2: 22 | print("Error: specify an IP to connect to!") 23 | exit(0) 24 | 25 | ip = sys.argv[1] 26 | 27 | NetworkTables.initialize(server=ip) 28 | 29 | 30 | class SomeClient(object): 31 | """Demonstrates an object with magic networktables properties""" 32 | 33 | robotTime = ntproperty("/SmartDashboard/robotTime", 0, writeDefault=False) 34 | 35 | dsTime = ntproperty("/SmartDashboard/dsTime", 0) 36 | 37 | 38 | c = SomeClient() 39 | 40 | 41 | i = 0 42 | while True: 43 | 44 | # equivalent to wpilib.SmartDashboard.getNumber('robotTime', None) 45 | print("robotTime:", c.robotTime) 46 | 47 | # equivalent to wpilib.SmartDashboard.putNumber('dsTime', i) 48 | c.dsTime = i 49 | 50 | time.sleep(1) 51 | i += 1 52 | -------------------------------------------------------------------------------- /tests/test_seqnum.py: -------------------------------------------------------------------------------- 1 | from _pynetworktables._impl.storage import _Entry 2 | 3 | 4 | def test_sequence_numbers(): 5 | 6 | e = _Entry("name", 0, None) 7 | 8 | # 9 | # Test rollover 10 | # 11 | 12 | e.seq_num = 0xFFFE 13 | e.increment_seqnum() 14 | assert e.seq_num == 0xFFFF 15 | 16 | e.increment_seqnum() 17 | assert e.seq_num == 0 18 | 19 | # 20 | # test Entry.isSeqNewerThan 21 | # -> operator > 22 | # 23 | 24 | e.seq_num = 10 25 | assert e.isSeqNewerThan(20) == False 26 | 27 | e.seq_num = 20 28 | assert e.isSeqNewerThan(10) == True 29 | 30 | e.seq_num = 50000 31 | assert e.isSeqNewerThan(10) == False 32 | 33 | e.seq_num = 10 34 | assert e.isSeqNewerThan(50000) == True 35 | 36 | e.seq_num = 20 37 | assert e.isSeqNewerThan(20) == False 38 | 39 | e.seq_num = 50000 40 | assert e.isSeqNewerThan(50000) == False 41 | 42 | # 43 | # test Entry.isSeqNewerOrEqual 44 | # -> operator >= 45 | # 46 | 47 | e.seq_num = 10 48 | assert e.isSeqNewerOrEqual(20) == False 49 | 50 | e.seq_num = 20 51 | assert e.isSeqNewerOrEqual(10) == True 52 | 53 | e.seq_num = 50000 54 | assert e.isSeqNewerOrEqual(10) == False 55 | 56 | e.seq_num = 10 57 | assert e.isSeqNewerOrEqual(50000) == True 58 | 59 | e.seq_num = 20 60 | assert e.isSeqNewerOrEqual(20) == True 61 | 62 | e.seq_num = 50000 63 | assert e.isSeqNewerOrEqual(50000) == True 64 | -------------------------------------------------------------------------------- /samples/json_logger/plot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # This example uses matplotlib to graph data received from the logger example. 4 | # It is assumed that the key being logged is a number array, and the first 5 | # item of that array is the Timer.getFPGATimestamp() on the robot. 6 | # 7 | # One example way to send the data on the robot is via something like 8 | # wpilib.SmartDashboard.putNumberArray([time, data1, data2, ...]) 9 | # 10 | 11 | import json 12 | import sys 13 | 14 | import matplotlib.pyplot as plt 15 | import numpy as np 16 | 17 | if __name__ == "__main__": 18 | with open(sys.argv[1]) as fp: 19 | data = json.load(fp) 20 | 21 | if len(data) == 0: 22 | print("No data received") 23 | exit(0) 24 | 25 | # Change the time to 26 | offset = data[0][0] 27 | for d in data: 28 | d[0] -= offset 29 | 30 | print("Received", len(data), "rows of data, total time was %.3f seconds" % d[0]) 31 | 32 | # Transform the data into a numpy array to make it easier to use 33 | data = np.array(data) 34 | 35 | # This allows you to use data[N] to refer to each column of data individually 36 | data = data.transpose() 37 | 38 | # This silly plot graphs 39 | # - x: data[0]: this is time 40 | # - y: data[1]: column1 41 | # 42 | # - x2: data[0]: Time yet again 43 | # - y2: data[1] - data[2]: subtracts columns from each other (something numpy allows) 44 | plt.plot(data[0], data[1], data[0], data[1] - data[2]) 45 | plt.title("Encoder error") 46 | 47 | plt.show() 48 | -------------------------------------------------------------------------------- /samples/json_logger/robot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Sample RobotPy program that demonstrates how to send data to the logger 4 | # example program 5 | # 6 | # This is NOT REQUIRED to use the example, any robot language can be used! 7 | # Translate the necessary bits into your own robot code.s 8 | # 9 | 10 | import wpilib 11 | 12 | 13 | class MyRobot(wpilib.IterativeRobot): 14 | """Main robot class""" 15 | 16 | def robotInit(self): 17 | """Robot-wide initialization code should go here""" 18 | 19 | def autonomousInit(self): 20 | """Called only at the beginning of autonomous mode""" 21 | self.useless = 1 22 | 23 | def autonomousPeriodic(self): 24 | """Called every 20ms in autonomous mode""" 25 | self.useless += 1 26 | 27 | # Obviously, this is fabricated... do something more useful! 28 | data1 = self.useless 29 | data2 = self.useless * 2 30 | 31 | # Only write once per loop 32 | wpilib.SmartDashboard.putNumberArray( 33 | "log_data", [wpilib.Timer.getFPGATimestamp(), data1, data2] 34 | ) 35 | 36 | def disabledInit(self): 37 | """Called only at the beginning of disabled mode""" 38 | pass 39 | 40 | def disabledPeriodic(self): 41 | """Called every 20ms in disabled mode""" 42 | pass 43 | 44 | def teleopInit(self): 45 | """Called only at the beginning of teleoperated mode""" 46 | pass 47 | 48 | def teleopPeriodic(self): 49 | """Called every 20ms in teleoperated mode""" 50 | 51 | 52 | if __name__ == "__main__": 53 | wpilib.run(MyRobot) 54 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | * Copyright (c) 2009-2018 FIRST 2 | * Copyright (c) 2015-2018 Dustin Spicuzza 3 | * Copyright (c) 2009-2018 Various contributors 4 | * All rights reserved. 5 | * 6 | * Redistribution and use in source and binary forms, with or without 7 | * modification, are permitted provided that the following conditions are met: 8 | * * Redistributions of source code must retain the above copyright 9 | * notice, this list of conditions and the following disclaimer. 10 | * * Redistributions in binary form must reproduce the above copyright 11 | * notice, this list of conditions and the following disclaimer in the 12 | * documentation and/or other materials provided with the distribution. 13 | * * Neither the name of the FIRST nor the 14 | * names of its contributors may be used to endorse or promote products 15 | * derived from this software without specific prior written permission. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY FIRST AND CONTRIBUTORS``AS IS'' AND ANY 18 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | * WARRANTIES OF MERCHANTABILITY NONINFRINGEMENT AND FITNESS FOR A PARTICULAR 20 | * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL FIRST OR CONTRIBUTORS BE LIABLE FOR 21 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 24 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /_pynetworktables/_impl/support/safe_thread.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | import logging 4 | 5 | logger = logging.getLogger("nt.th") 6 | 7 | 8 | class SafeThread(object): 9 | """ 10 | Not exactly the same as wpiutil SafeThread... exists so we don't have 11 | to duplicate functionality in a lot of places 12 | """ 13 | 14 | # Name each thread uniquely to make debugging easier 15 | _global_indices_lock = threading.Lock() 16 | _global_indices = {} 17 | 18 | def __init__(self, target, name, args=()): 19 | """ 20 | Note: thread is automatically started and daemonized 21 | """ 22 | 23 | with SafeThread._global_indices_lock: 24 | idx = SafeThread._global_indices.setdefault(name, -1) + 1 25 | SafeThread._global_indices[name] = idx 26 | name = "%s-%s" % (name, idx) 27 | 28 | self.name = name 29 | 30 | self._thread = threading.Thread( 31 | target=self._run, name=name, args=(target, args) 32 | ) 33 | self._thread.daemon = True 34 | 35 | self.is_alive = self._thread.is_alive 36 | self.join = self._thread.join 37 | 38 | self._thread.start() 39 | 40 | def join(self, timeout=1): 41 | self._thread.join(timeout=timeout) 42 | if not self._thread.is_alive(): 43 | logger.warning("Thread %s did not stop!", self.name) 44 | 45 | def _run(self, target, args): 46 | logger.debug("Started thread %s", self.name) 47 | try: 48 | target(*args) 49 | except Exception: 50 | logger.warning("Thread %s died unexpectedly", self.name, exc_info=True) 51 | else: 52 | logger.debug("Thread %s exited", self.name) 53 | -------------------------------------------------------------------------------- /_pynetworktables/_impl/connection_notifier.py: -------------------------------------------------------------------------------- 1 | # validated: 2018-11-27 DS ac751d32247e cpp/ConnectionNotifier.cpp cpp/ConnectionNotifier.h cpp/IConnectionNotifier.h 2 | # ---------------------------------------------------------------------------- 3 | # Copyright (c) FIRST 2017. All Rights Reserved. 4 | # Open Source Software - may be modified and shared by FRC teams. The code 5 | # must be accompanied by the FIRST BSD license file in the root directory of 6 | # the project. 7 | # ---------------------------------------------------------------------------- 8 | 9 | from collections import namedtuple 10 | 11 | from .callback_manager import CallbackManager, CallbackThread 12 | 13 | _ConnectionCallback = namedtuple("ConnectionCallback", ["callback", "poller_uid"]) 14 | 15 | _ConnectionNotification = namedtuple( 16 | "ConnectionNotification", ["connected", "conn_info"] 17 | ) 18 | 19 | 20 | class ConnectionNotifierThread(CallbackThread): 21 | def __init__(self): 22 | CallbackThread.__init__(self, "connection-notifier") 23 | 24 | def matches(self, listener, data): 25 | return True 26 | 27 | def setListener(self, data, listener_uid): 28 | pass 29 | 30 | def doCallback(self, callback, data): 31 | callback(data) 32 | 33 | 34 | class ConnectionNotifier(CallbackManager): 35 | 36 | THREAD_CLASS = ConnectionNotifierThread 37 | 38 | def add(self, callback): 39 | return self.doAdd(_ConnectionCallback(callback, None)) 40 | 41 | def addPolled(self, poller_uid): 42 | return self.doAdd(_ConnectionCallback(None, poller_uid)) 43 | 44 | def notifyConnection(self, connected, conn_info, only_listener=None): 45 | self.send(only_listener, _ConnectionNotification(connected, conn_info)) 46 | 47 | def start(self): 48 | CallbackManager.start(self) 49 | -------------------------------------------------------------------------------- /tests/test_uleb128.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from io import BytesIO 3 | 4 | import pytest 5 | 6 | from _pynetworktables._impl.support.leb128 import ( 7 | size_uleb128, 8 | encode_uleb128, 9 | read_uleb128, 10 | ) 11 | 12 | 13 | def test_size(): 14 | 15 | # Testing Plan: 16 | # (1) 128 ^ n ............ need (n+1) bytes 17 | # (2) 128 ^ n * 64 ....... need (n+1) bytes 18 | # (3) 128 ^ (n+1) - 1 .... need (n+1) bytes 19 | 20 | assert 1 == size_uleb128(0) # special case 21 | 22 | assert 1 == size_uleb128(0x1) 23 | assert 1 == size_uleb128(0x40) 24 | assert 1 == size_uleb128(0x7F) 25 | 26 | assert 2 == size_uleb128(0x80) 27 | assert 2 == size_uleb128(0x2000) 28 | assert 2 == size_uleb128(0x3FFF) 29 | 30 | assert 3 == size_uleb128(0x4000) 31 | assert 3 == size_uleb128(0x100000) 32 | assert 3 == size_uleb128(0x1FFFFF) 33 | 34 | assert 4 == size_uleb128(0x200000) 35 | assert 4 == size_uleb128(0x8000000) 36 | assert 4 == size_uleb128(0xFFFFFFF) 37 | 38 | assert 5 == size_uleb128(0x10000000) 39 | assert 5 == size_uleb128(0x40000000) 40 | assert 5 == size_uleb128(0x7FFFFFFF) 41 | 42 | 43 | def test_wikipedia_example(): 44 | 45 | i = 624485 46 | result_bytes = bytes(bytearray([0xE5, 0x8E, 0x26])) 47 | 48 | bio = BytesIO(encode_uleb128(i)) 49 | print(result_bytes) 50 | assert bio.read() == result_bytes 51 | bio.seek(0) 52 | 53 | result_i = read_uleb128(bio) 54 | 55 | assert result_i == i 56 | 57 | 58 | @pytest.mark.parametrize( 59 | "i", [0, 1, 16, 42, 65, 0xFF, 0xFFF, 0xFFFFFFFF, 0x123456789, 100000000000000000000] 60 | ) 61 | def test_roundtrip(i): 62 | 63 | bio = BytesIO(encode_uleb128(i)) 64 | print(bio.read()) 65 | bio.seek(0) 66 | 67 | r = read_uleb128(bio) 68 | 69 | print(r) 70 | 71 | assert r == i 72 | -------------------------------------------------------------------------------- /_pynetworktables/_impl/tcpsockets/tcp_stream.py: -------------------------------------------------------------------------------- 1 | # novalidate 2 | 3 | import select 4 | import socket 5 | import threading 6 | 7 | 8 | class StreamEOF(IOError): 9 | pass 10 | 11 | 12 | class TCPStream(object): 13 | def __init__(self, sd, peer_ip, peer_port, sock_type): 14 | 15 | self.m_sd = sd 16 | self.m_peerIP = peer_ip 17 | self.m_peerPort = peer_port 18 | 19 | self.m_rdsock = sd.makefile("rb") 20 | self.m_wrsock = sd.makefile("wb") 21 | 22 | self.close_lock = threading.Lock() 23 | 24 | # Python-specific for debugging 25 | self.sock_type = sock_type 26 | 27 | def read(self, size): 28 | 29 | # TODO: ntcore does a select to wait for read to be available. Necessary? 30 | 31 | data = self.m_rdsock.read(size) 32 | if size > 0 and len(data) != size: 33 | raise StreamEOF("end of file") 34 | return data 35 | 36 | def readline(self): 37 | return self.m_rdsock.readline() 38 | 39 | def readStruct(self, s): 40 | sz = s.size 41 | data = self.m_rdsock.read(sz) 42 | if len(data) != sz: 43 | raise StreamEOF("end of file") 44 | return s.unpack(data) 45 | 46 | def send(self, contents): 47 | self.m_wrsock.write(contents) 48 | self.m_wrsock.flush() 49 | 50 | def close(self): 51 | with self.close_lock: 52 | if self.m_sd: 53 | try: 54 | self.m_sd.shutdown(socket.SHUT_RDWR) 55 | except OSError: 56 | pass 57 | 58 | self.m_sd.close() 59 | # self.m_sd = None 60 | 61 | def getPeerIP(self): 62 | return self.m_peerIP 63 | 64 | def getPeerPort(self): 65 | return self.m_peerPort 66 | 67 | def setNoDelay(self): 68 | self.m_sd.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) 69 | 70 | def _waitForReadEvent(self, timeout): 71 | r, _, _ = select.select((self.m_sd,), (), (), timeout) 72 | return len(r) > 0 73 | -------------------------------------------------------------------------------- /_pynetworktables/_impl/tcpsockets/tcp_connector.py: -------------------------------------------------------------------------------- 1 | # novalidate 2 | 3 | import socket 4 | import threading 5 | 6 | from .tcp_stream import TCPStream 7 | 8 | import logging 9 | 10 | logger = logging.getLogger("nt.net") 11 | 12 | 13 | class TcpConnector(object): 14 | def __init__(self, timeout, verbose): 15 | self.cond = threading.Condition() 16 | self.threads = {} 17 | self.active = False 18 | self.result = None 19 | self.timeout = timeout 20 | self.verbose = verbose 21 | 22 | def setVerbose(self, verbose): 23 | self.verbose = verbose 24 | 25 | def connect(self, server_or_servers): 26 | if isinstance(server_or_servers, tuple): 27 | server, port = server_or_servers 28 | return self._connect(server, port) 29 | 30 | # parallel connect 31 | # -> only connect to servers that aren't currently being connected to 32 | with self.cond: 33 | self.active = True 34 | for item in server_or_servers: 35 | if item not in self.threads: 36 | th = threading.Thread( 37 | target=self._thread, args=item, name="TcpConnector" 38 | ) 39 | th.daemon = True 40 | th.start() 41 | self.threads[item] = th 42 | 43 | self.cond.wait(2 * self.timeout) 44 | self.active = False 45 | 46 | result = self.result 47 | self.result = None 48 | return result 49 | 50 | def _thread(self, server, port): 51 | stream = self._connect(server, port) 52 | with self.cond: 53 | self.threads.pop((server, port), None) 54 | if self.active and self.result is None: 55 | self.result = stream 56 | self.cond.notify() 57 | 58 | def _connect(self, server, port): 59 | try: 60 | if self.verbose: 61 | logger.debug("Trying connection to %s:%s", server, port) 62 | 63 | if self.timeout is None: 64 | sd = socket.create_connection((server, port)) 65 | else: 66 | sd = socket.create_connection((server, port), timeout=self.timeout) 67 | sd.settimeout(None) 68 | 69 | return TCPStream(sd, server, port, "client") 70 | except IOError: 71 | if self.verbose: 72 | logger.debug("Connection to %s:%s failed", server, port) 73 | return 74 | -------------------------------------------------------------------------------- /.github/workflows/dist.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: dist 3 | 4 | on: 5 | pull_request: 6 | push: 7 | branches: 8 | - main 9 | tags: 10 | - '*' 11 | 12 | jobs: 13 | check: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: psf/black@stable 18 | 19 | build: 20 | runs-on: ubuntu-18.04 21 | steps: 22 | - uses: actions/checkout@v1 23 | - uses: actions/setup-python@v1 24 | with: 25 | python-version: 3.6 26 | 27 | - name: Install build dependencies 28 | run: | 29 | python -m pip install wheel 30 | 31 | - uses: robotpy/build-actions/build-sdist@v2021 32 | - uses: robotpy/build-actions/build-wheel@v2021 33 | 34 | - name: Upload build artifacts 35 | uses: actions/upload-artifact@v2 36 | with: 37 | name: dist 38 | path: dist 39 | 40 | test: 41 | needs: [build] 42 | runs-on: ${{ matrix.os }} 43 | strategy: 44 | fail-fast: false 45 | matrix: 46 | backend: [pure, pyntcore] 47 | os: [windows-latest, macos-latest, ubuntu-18.04] 48 | python_version: [3.6, 3.7, 3.8, 3.9] 49 | architecture: [x86, x64] 50 | exclude: 51 | - os: macos-latest 52 | architecture: x86 53 | - os: ubuntu-18.04 54 | architecture: x86 55 | 56 | steps: 57 | - uses: actions/checkout@v1 58 | - uses: actions/setup-python@v1 59 | with: 60 | python-version: ${{ matrix.python_version }} 61 | architecture: ${{ matrix.architecture }} 62 | 63 | - name: Download build artifacts 64 | uses: actions/download-artifact@v2 65 | with: 66 | name: dist 67 | path: dist 68 | 69 | - name: Install pyntcore backend 70 | if: ${{ matrix.backend == 'pyntcore' }} 71 | run: | 72 | python -m pip install pyntcore --find-links https://www.tortall.net/\~robotpy/wheels/2021/linux_x86_64/ 73 | 74 | - uses: robotpy/build-actions/test-native-wheel@v2021 75 | # continue-on-error: ${{ matrix.backend == 'pyntcore' }} 76 | 77 | publish: 78 | runs-on: ubuntu-latest 79 | needs: [check, test] 80 | if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') 81 | 82 | steps: 83 | - name: Download build artifacts 84 | uses: actions/download-artifact@v2 85 | with: 86 | name: dist 87 | path: dist 88 | 89 | - name: Publish to PyPI 90 | uses: pypa/gh-action-pypi-publish@master 91 | with: 92 | user: __token__ 93 | password: ${{ secrets.pypi_password }} 94 | -------------------------------------------------------------------------------- /samples/benchmark.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Simple benchmark script that tests NetworkTables throughput. 4 | # 5 | # To run the server 6 | # 7 | # python3 benchmark.py 8 | # 9 | # To run the client: 10 | # 11 | # python3 benchmark.py 127.0.0.1 12 | # 13 | # In theory, the limiting factor is internal buffering and not network 14 | # bandwidth, so running this test on a local machine should be mostly 15 | # valid. 16 | # 17 | # On OSX with default write flush period, I average ~18hz 18 | # 19 | 20 | from __future__ import print_function 21 | 22 | from argparse import ArgumentParser 23 | import time 24 | 25 | import logging 26 | 27 | logging.basicConfig(level=logging.DEBUG) 28 | 29 | from networktables import NetworkTables 30 | 31 | 32 | class Benchmark(object): 33 | def __init__(self): 34 | 35 | parser = ArgumentParser() 36 | parser.add_argument("client", nargs="?", default=None) 37 | parser.add_argument("-r", "--rate", default=None, type=float) 38 | parser.add_argument("--send", default=False, action="store_true") 39 | 40 | args = parser.parse_args() 41 | 42 | if args.client: 43 | NetworkTables.initialize(server=args.client) 44 | else: 45 | NetworkTables.initialize() 46 | 47 | # Default write flush is 0.05, could adjust for less latency 48 | if args.rate is not None: 49 | print("Setting rate to %s" % args.rate) 50 | NetworkTables.setUpdateRate(args.rate) 51 | 52 | self.nt = NetworkTables.getTable("/benchmark") 53 | self.updates = 0 54 | 55 | self.nt.addTableListener(self.on_update) 56 | 57 | if args.send: 58 | self.send_benchmark() 59 | else: 60 | self.recv_benchmark() 61 | 62 | def on_update(self, *args): 63 | self.updates += 1 64 | 65 | def recv_benchmark(self): 66 | 67 | print("Starting to receive") 68 | 69 | last = None 70 | last_updates = 0 71 | 72 | while True: 73 | 74 | now = time.time() 75 | updates = self.updates 76 | 77 | if last is not None: 78 | rate = (updates - last_updates) / (now - last) 79 | print("Update rate:", rate) 80 | 81 | last = now 82 | last_updates = updates 83 | time.sleep(1) 84 | 85 | def send_benchmark(self): 86 | 87 | print("Sending") 88 | 89 | i = 0 90 | 91 | while True: 92 | i += 1 93 | self.nt.putNumber("key", i) 94 | time.sleep(0.0001) 95 | 96 | 97 | if __name__ == "__main__": 98 | Benchmark() 99 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | from os.path import dirname, exists, join 4 | import sys, subprocess 5 | 6 | from setuptools import find_packages, setup 7 | 8 | setup_dir = dirname(__file__) 9 | git_dir = join(setup_dir, ".git") 10 | version_file = join(setup_dir, "_pynetworktables", "_impl", "version.py") 11 | 12 | # Automatically generate a version.py based on the git version 13 | if exists(git_dir): 14 | p = subprocess.Popen( 15 | ["git", "describe", "--tags", "--long", "--dirty=-dirty"], 16 | stdout=subprocess.PIPE, 17 | stderr=subprocess.PIPE, 18 | ) 19 | out, err = p.communicate() 20 | # Make sure the git version has at least one tag 21 | if err: 22 | print("Error: You need to create a tag for this repo to use the builder") 23 | sys.exit(1) 24 | 25 | # Convert git version to PEP440 compliant version 26 | # - Older versions of pip choke on local identifiers, so we can't include the git commit 27 | v, commits, local = out.decode("utf-8").rstrip().split("-", 2) 28 | if commits != "0" or "-dirty" in local: 29 | v = "%s.post0.dev%s" % (v, commits) 30 | 31 | # Create the version.py file 32 | with open(version_file, "w") as fp: 33 | fp.write("# Autogenerated by setup.py\n__version__ = '{0}'".format(v)) 34 | 35 | with open(version_file, "r") as fp: 36 | exec(fp.read(), globals()) 37 | 38 | with open(join(setup_dir, "README.rst"), "r") as readme_file: 39 | long_description = readme_file.read() 40 | 41 | setup( 42 | name="pynetworktables", 43 | version=__version__, 44 | description="A pure Python implementation of NetworkTables, used for robot communications in the FIRST Robotics Competition.", 45 | long_description=long_description, 46 | author="Dustin Spicuzza, Peter Johnson", 47 | author_email="robotpy@googlegroups.com", 48 | url="https://github.com/robotpy/pynetworktables", 49 | keywords="frc first robotics wpilib networktables", 50 | packages=find_packages(exclude="tests"), 51 | package_data={ 52 | "networktables": ["py.typed", "__init__.pyi"], 53 | "_pynetworktables": ["py.typed"], 54 | }, 55 | python_requires=">=3.5", 56 | license="BSD-3-Clause", 57 | classifiers=[ 58 | "Development Status :: 5 - Production/Stable", 59 | "Intended Audience :: Developers", 60 | "Intended Audience :: Education", 61 | "License :: OSI Approved :: BSD License", 62 | "Operating System :: OS Independent", 63 | "Programming Language :: Python :: 3", 64 | "Programming Language :: Python :: 3.5", 65 | "Programming Language :: Python :: 3.6", 66 | "Programming Language :: Python :: 3.7", 67 | "Programming Language :: Python :: 3.8", 68 | "Topic :: Scientific/Engineering", 69 | ], 70 | ) 71 | -------------------------------------------------------------------------------- /tests/test_conn_listener.py: -------------------------------------------------------------------------------- 1 | # ---------------------------------------------------------------------------- 2 | # Copyright (c) FIRST 2017. All Rights Reserved. 3 | # Open Source Software - may be modified and shared by FRC teams. The code 4 | # must be accompanied by the FIRST BSD license file in the root directory of 5 | # the project. 6 | # ---------------------------------------------------------------------------- 7 | 8 | # 9 | # These tests are adapted from ntcore's test suite 10 | # 11 | 12 | import threading 13 | 14 | 15 | def test_Polled(nt_server, nt_client): 16 | nt_server_api = nt_server._api 17 | 18 | if nt_server.proto_rev == 0x0200 or nt_client.proto_rev == 0x0200: 19 | # this is annoying to test because of the reconnect 20 | return 21 | 22 | # set up the poller 23 | poller = nt_server_api.createConnectionListenerPoller() 24 | handle = nt_server_api.addPolledConnectionListener(poller, False) 25 | 26 | # trigger a connect event 27 | nt_server.start_test() 28 | nt_client.start_test() 29 | 30 | # get the event 31 | assert nt_server_api.waitForConnectionListenerQueue(1.0) 32 | 33 | result, timed_out = nt_server_api.pollConnectionListener(poller, 1.0) 34 | assert not timed_out 35 | assert len(result) == 1 36 | assert handle == result[0][0] 37 | assert result[0][1].connected 38 | del result[:] 39 | 40 | # trigger a disconnect event 41 | nt_client.shutdown() 42 | 43 | # get the event 44 | assert nt_server_api.waitForConnectionListenerQueue(1.0) 45 | 46 | result, timed_out = nt_server_api.pollConnectionListener(poller, 0.1) 47 | assert not timed_out 48 | assert len(result) == 1 49 | assert handle == result[0][0] 50 | assert not result[0][1].connected 51 | 52 | 53 | def test_Threaded(nt_server, nt_client): 54 | nt_server_api = nt_server._api 55 | 56 | if nt_server.proto_rev == 0x0200 or nt_client.proto_rev == 0x0200: 57 | # this is annoying to test because of the reconnect 58 | return 59 | 60 | result_cond = threading.Condition() 61 | result = [] 62 | 63 | def _server_cb(event): 64 | with result_cond: 65 | result.append(event) 66 | result_cond.notify() 67 | 68 | nt_server_api.addConnectionListener(_server_cb, False) 69 | 70 | # trigger a connect event 71 | nt_server.start_test() 72 | nt_client.start_test() 73 | 74 | with result_cond: 75 | result_cond.wait(0.5) 76 | 77 | # get the event 78 | assert len(result) == 1 79 | # assert handle == result[0].listener 80 | assert result[0].connected 81 | del result[:] 82 | 83 | # trigger a disconnect event 84 | nt_client.shutdown() 85 | 86 | with result_cond: 87 | result_cond.wait(0.5) 88 | 89 | # get the event 90 | assert len(result) == 1 91 | # assert handle == result[0].listener 92 | assert not result[0].connected 93 | -------------------------------------------------------------------------------- /_pynetworktables/_impl/constants.py: -------------------------------------------------------------------------------- 1 | # novalidate 2 | # fmt: off 3 | 4 | # data types 5 | NT_UNASSIGNED = b'\x00' 6 | NT_BOOLEAN = b'\x01' 7 | NT_DOUBLE = b'\x02' 8 | NT_STRING = b'\x04' 9 | NT_RAW = b'\x08' 10 | NT_BOOLEAN_ARRAY = b'\x10' 11 | NT_DOUBLE_ARRAY = b'\x20' 12 | NT_STRING_ARRAY = b'\x40' 13 | NT_RPC = b'\x80' 14 | 15 | # Raw types transmitted on the wire 16 | NT_VTYPE2RAW = { 17 | NT_BOOLEAN: b'\x00', 18 | NT_DOUBLE: b'\x01', 19 | NT_STRING: b'\x02', 20 | NT_RAW: b'\x03', 21 | NT_BOOLEAN_ARRAY: b'\x10', 22 | NT_DOUBLE_ARRAY: b'\x11', 23 | NT_STRING_ARRAY: b'\x12', 24 | NT_RPC: b'\x20', 25 | } 26 | 27 | NT_RAW2VTYPE = {v: k for k, v in NT_VTYPE2RAW.items()} 28 | 29 | 30 | 31 | # NetworkTables notifier kinds. 32 | NT_NOTIFY_NONE = 0x00 33 | NT_NOTIFY_IMMEDIATE = 0x01 # initial listener addition 34 | NT_NOTIFY_LOCAL = 0x02 # changed locally 35 | NT_NOTIFY_NEW = 0x04 # newly created entry 36 | NT_NOTIFY_DELETE = 0x08 # deleted 37 | NT_NOTIFY_UPDATE = 0x10 # value changed 38 | NT_NOTIFY_FLAGS = 0x20 # flags changed 39 | 40 | # Client/server modes 41 | NT_NET_MODE_NONE = 0x00 # not running 42 | NT_NET_MODE_SERVER = 0x01 # running in server mode 43 | NT_NET_MODE_CLIENT = 0x02 # running in client mode 44 | NT_NET_MODE_STARTING = 0x04 # flag for starting (either client or server) 45 | NT_NET_MODE_FAILURE = 0x08 # flag for failure (either client or server) 46 | NT_NET_MODE_TEST = 0x10 # flag indicating test mode (either client or server) 47 | 48 | # NetworkTables entry flags 49 | NT_PERSISTENT = 0x01 50 | 51 | 52 | # Message types 53 | kKeepAlive = b'\x00' 54 | kClientHello = b'\x01' 55 | kProtoUnsup = b'\x02' 56 | kServerHelloDone = b'\x03' 57 | kServerHello = b'\x04' 58 | kClientHelloDone = b'\x05' 59 | kEntryAssign = b'\x10' 60 | kEntryUpdate = b'\x11' 61 | kFlagsUpdate = b'\x12' 62 | kEntryDelete = b'\x13' 63 | kClearEntries = b'\x14' 64 | kExecuteRpc = b'\x20' 65 | kRpcResponse = b'\x21' 66 | 67 | kClearAllMagic = 0xD06CB27A 68 | 69 | _msgtypes = { 70 | kKeepAlive: 'kKeepAlive', 71 | kClientHello: 'kClientHello', 72 | kProtoUnsup: 'kProtoUnsup', 73 | kServerHelloDone: 'kServerHelloDone', 74 | kServerHello: 'kServerHello', 75 | kClientHelloDone: 'kClientHelloDone', 76 | kEntryAssign: 'kEntryAssign', 77 | kEntryUpdate: 'kEntryUpdate', 78 | kFlagsUpdate: 'kFlagsUpdate', 79 | kEntryDelete: 'kEntryDelete', 80 | kClearEntries: 'kClearEntries', 81 | kExecuteRpc: 'kExecuteRpc', 82 | kRpcResponse: 'kRpcResponse', 83 | } 84 | 85 | def msgtype_str(msgtype): 86 | return _msgtypes.get(msgtype, 'Unknown (%s)' % msgtype) 87 | 88 | # The default port that network tables operates on 89 | NT_DEFAULT_PORT = 1735 90 | -------------------------------------------------------------------------------- /docs/gensidebar.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file generates the sidebar/toctree for all RobotPy projects and should 3 | # be copied to each project when it is updated 4 | # 5 | 6 | import os 7 | 8 | 9 | def write_if_changed(fname, contents): 10 | 11 | try: 12 | with open(fname, "r") as fp: 13 | old_contents = fp.read() 14 | except: 15 | old_contents = "" 16 | 17 | if old_contents != contents: 18 | with open(fname, "w") as fp: 19 | fp.write(contents) 20 | 21 | 22 | def generate_sidebar(conf, conf_api): 23 | 24 | # determine 'latest' or 'stable' 25 | # if not conf.do_gen: 26 | do_gen = os.environ.get("SIDEBAR", None) == "1" or conf["on_rtd"] 27 | version = conf["rtd_version"] 28 | 29 | lines = ["", ".. DO NOT MODIFY! THIS PAGE IS AUTOGENERATED!", ""] 30 | 31 | def toctree(name): 32 | lines.extend( 33 | [".. toctree::", " :caption: %s" % name, " :maxdepth: 2", ""] 34 | ) 35 | 36 | def endl(): 37 | lines.append("") 38 | 39 | def write(desc, link): 40 | if conf_api == "robotpy": 41 | args = desc, link 42 | elif not do_gen: 43 | return 44 | else: 45 | args = ( 46 | desc, 47 | "https://robotpy.readthedocs.io/en/%s/%s.html" % (version, link), 48 | ) 49 | 50 | lines.append(" %s <%s>" % args) 51 | 52 | def write_api(project, desc): 53 | if project != conf_api: 54 | if do_gen: 55 | args = desc, project, version 56 | lines.append( 57 | " %s API " 58 | % args 59 | ) 60 | else: 61 | lines.append(" %s API " % desc) 62 | 63 | # 64 | # Specify the sidebar contents here 65 | # 66 | 67 | toctree("Robot Programming") 68 | write("Getting Started", "getting_started") 69 | write("Installation", "install/index") 70 | write("Programmer's Guide", "guide/index") 71 | write("Frameworks", "frameworks/index") 72 | write("Hardware & Sensors", "hw") 73 | write("Camera & Vision", "vision/index") 74 | endl() 75 | 76 | toctree("API Reference") 77 | write_api("wpilib", "WPILib") 78 | write_api("pynetworktables", "NetworkTables") 79 | write_api("cscore", "CSCore") 80 | write_api("utilities", "Utilities") 81 | write_api("pyfrc", "PyFRC") 82 | write_api("ctre", "CTRE Libraries") 83 | write_api("navx", "NavX Library") 84 | write_api("rev", "SPARK MAX Library") 85 | endl() 86 | 87 | toctree("Additional Info") 88 | write("Troubleshooting", "troubleshooting") 89 | write("Support", "support") 90 | write("FAQ", "faq") 91 | endl() 92 | 93 | toctree("RobotPy Developers") 94 | write("Developer Documentation", "dev/index") 95 | endl() 96 | 97 | write_if_changed("_sidebar.rst.inc", "\n".join(lines)) 98 | -------------------------------------------------------------------------------- /_pynetworktables/_impl/rpc_server.py: -------------------------------------------------------------------------------- 1 | # validated: 2018-11-27 DS ac751d32247e cpp/RpcServer.cpp cpp/RpcServer.h cpp/IRpcServer.h 2 | # ---------------------------------------------------------------------------- 3 | # Copyright (c) FIRST 2017. All Rights Reserved. 4 | # Open Source Software - may be modified and shared by FRC teams. The code 5 | # must be accompanied by the FIRST BSD license file in the root directory of 6 | # the project. 7 | # ---------------------------------------------------------------------------- 8 | 9 | from collections import namedtuple 10 | 11 | from .callback_manager import CallbackManager, CallbackThread 12 | from .message import Message 13 | 14 | import logging 15 | 16 | logger = logging.getLogger("nt") 17 | 18 | _RpcListenerData = namedtuple("RpcListenerData", ["callback", "poller_uid"]) 19 | 20 | _RpcCall = namedtuple( 21 | "RpcCall", ["local_id", "call_uid", "name", "params", "conn_info", "send_response"] 22 | ) 23 | 24 | 25 | class RpcServerThread(CallbackThread): 26 | def __init__(self): 27 | CallbackThread.__init__(self, "rpc-server") 28 | self.m_response_map = {} 29 | 30 | def matches(self, listener, data): 31 | return data.name and data.send_response 32 | 33 | def setListener(self, data, listener_uid): 34 | lookup_id = (data.local_id, data.call_uid) 35 | self.m_response_map[lookup_id] = data.send_response 36 | 37 | def doCallback(self, callback, data): 38 | local_id = data.local_id 39 | call_uid = data.call_uid 40 | lookup_id = (data.local_id, data.call_uid) 41 | callback(data) 42 | 43 | # send empty response 44 | send_response = self.m_response_map.get(lookup_id) 45 | if send_response: 46 | send_response(Message.rpcResponse(local_id, call_uid, "")) 47 | 48 | 49 | class RpcServer(CallbackManager): 50 | 51 | THREAD_CLASS = RpcServerThread 52 | 53 | def add(self, callback): 54 | return self.doAdd(_RpcListenerData(callback, None)) 55 | 56 | def addPolled(self, poller_uid): 57 | return self.doAdd(_RpcListenerData(None, poller_uid)) 58 | 59 | def removeRpc(self, rpc_uid): 60 | return self.remove(rpc_uid) 61 | 62 | def processRpc( 63 | self, local_id, call_uid, name, params, conn_info, send_response, rpc_uid 64 | ): 65 | call = _RpcCall(local_id, call_uid, name, params, conn_info, send_response) 66 | self.send(rpc_uid, call) 67 | 68 | def postRpcResponse(self, local_id, call_uid, result): 69 | thr = self.m_owner 70 | response = thr.m_response_map.pop((local_id, call_uid), None) 71 | if response is None: 72 | logger.warning( 73 | "Posting RPC response to nonexistent call (or duplicate response)" 74 | ) 75 | return False 76 | else: 77 | response(Message.rpcResponse(local_id, call_uid, result)) 78 | return True 79 | 80 | def start(self): 81 | CallbackManager.start(self) 82 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | RobotPy NetworkTables Project 2 | ============================= 3 | 4 | This is a pure python implementation of the NetworkTables protocol, derived 5 | from the wpilib ntcore C++ implementation. In FRC, the NetworkTables protocol 6 | is used to pass non-Driver Station data to and from the robot across the network. 7 | 8 | This implementation is intended to be compatible with python 3.5 and later. 9 | All commits to the repository are automatically tested on all supported python 10 | versions using github actions. 11 | 12 | .. note:: NetworkTables is a protocol used for robot communication in the 13 | FIRST Robotics Competition, and can be used to talk to 14 | SmartDashboard/SFX. It does not have any security, and should never 15 | be used on untrusted networks. 16 | 17 | .. note:: If you require support for Python 2.7, use pynetworktables 2018.2.0 18 | 19 | .. important:: pynetworktables implements the NetworkTables 3 protocol, which is deprecated. 20 | It is not compatible with the 2027 control system. Use 21 | `pyntcore `_ 22 | instead. 23 | 24 | Documentation 25 | ------------- 26 | 27 | For usage, detailed installation information, and other notes, please see 28 | our documentation at http://pynetworktables.readthedocs.io 29 | 30 | Don't understand this NetworkTables thing? Check out our `basic overview of 31 | NetworkTables `_. 32 | 33 | Installation 34 | ------------ 35 | 36 | On the RoboRIO, you don't install this directly, but use the RobotPy installer 37 | to install it on your RoboRIO, or it is installed by pip as part of the 38 | pyfrc setup process. 39 | 40 | On something like a coprocessor, driver station, or laptop, make sure pip is 41 | installed, connect to the internet, and install like so: 42 | 43 | :: 44 | 45 | pip install pynetworktables 46 | 47 | Support 48 | ------- 49 | 50 | The RobotPy project has a mailing list that you can send emails to for 51 | support: robotpy@googlegroups.com. Keep in mind that the maintainers of 52 | RobotPy projects are also members of FRC Teams and do this in their free 53 | time. 54 | 55 | If you find a bug, please file a bug report using github 56 | https://github.com/robotpy/pynetworktables/issues/new 57 | 58 | Contributing new changes 59 | ------------------------ 60 | 61 | RobotPy is an open project that all members of the FIRST community can 62 | easily and quickly contribute to. If you find a bug, or have an idea that you 63 | think others can use: 64 | 65 | 1. `Fork this git repository `_ to your github account 66 | 2. Create your feature branch (`git checkout -b my-new-feature`) 67 | 3. Commit your changes (`git commit -am 'Add some feature'`) 68 | 4. Push to the branch (`git push -u origin my-new-feature`) 69 | 5. Create new Pull Request on github 70 | 71 | Authors & Contributors 72 | ====================== 73 | 74 | * Dustin Spicuzza, FRC Team 1418/2423 75 | * Peter Johnson, FRC Team 294 76 | -------------------------------------------------------------------------------- /_pynetworktables/_impl/tcpsockets/tcp_acceptor.py: -------------------------------------------------------------------------------- 1 | # novalidate 2 | 3 | import threading 4 | import socket 5 | 6 | from .tcp_stream import TCPStream 7 | 8 | import logging 9 | 10 | logger = logging.getLogger("nt") 11 | 12 | 13 | class TcpAcceptor(object): 14 | def __init__(self, port, address): 15 | # Protects open/shutdown/close 16 | # -> This is a condition to allow testing code to wait 17 | # for server startup 18 | self.lock = threading.Condition() 19 | 20 | self.m_lsd = None 21 | self.m_port = port 22 | self.m_address = address 23 | self.m_listening = False 24 | self.m_shutdown = False 25 | 26 | def waitForStart(self, timeout=None): 27 | with self.lock: 28 | if not self.m_listening: 29 | self.lock.wait(timeout=timeout) 30 | 31 | return self.m_listening 32 | 33 | def close(self): 34 | 35 | with self.lock: 36 | if self.m_lsd: 37 | self.shutdown() 38 | self.m_lsd.close() 39 | 40 | self.m_lsd = None 41 | 42 | def start(self): 43 | with self.lock: 44 | if self.m_listening: 45 | return False 46 | 47 | self.m_lsd = socket.socket() 48 | 49 | try: 50 | self.m_lsd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 51 | self.m_lsd.bind((self.m_address, self.m_port)) 52 | 53 | # needed for testing 54 | if self.m_port == 0: 55 | self.m_port = self.m_lsd.getsockname()[1] 56 | 57 | self.m_lsd.listen(10) 58 | except OSError: 59 | logger.exception("Error starting server") 60 | 61 | try: 62 | self.m_lsd.close() 63 | except Exception: 64 | pass 65 | 66 | self.m_lsd = None 67 | self.lock.notify() 68 | return False 69 | 70 | self.m_listening = True 71 | self.lock.notify() 72 | logger.debug("Listening on %s %s", self.m_address, self.m_port) 73 | return True 74 | 75 | def shutdown(self): 76 | with self.lock: 77 | if self.m_listening and not self.m_shutdown: 78 | self.m_shutdown = True 79 | self.m_listening = False 80 | try: 81 | self.m_lsd.shutdown(socket.SHUT_RDWR) 82 | except OSError: 83 | pass 84 | 85 | def accept(self): 86 | if not self.m_listening or self.m_shutdown: 87 | return 88 | 89 | try: 90 | sd, (peer_ip, peer_port) = self.m_lsd.accept() 91 | except OSError: 92 | if not self.m_shutdown: 93 | logger.warning("Error accepting connection", exc_info=True) 94 | return 95 | 96 | if self.m_shutdown: 97 | try: 98 | sd.close() 99 | except Exception: 100 | pass 101 | return 102 | 103 | return TCPStream(sd, peer_ip, peer_port, "server") 104 | -------------------------------------------------------------------------------- /tools/fuzzer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # A simplified fuzzer that can be used to try and discover errors in the 4 | # NetworkTables implementation. 5 | # 6 | # DON'T USE THIS ON A ROBOT CONNECTED TO REAL THINGS! Not all NetworkTables 7 | # implementations are robust, and may crash or malfunction when subjected 8 | # to this fuzzer 9 | # 10 | 11 | # 12 | # TODO: Do more evil things to tease out deadlock/other bugs 13 | # 14 | 15 | ip = "127.0.0.1" 16 | port = 1735 17 | 18 | import errno 19 | import threading 20 | import socket 21 | import os 22 | from random import choice, randint 23 | 24 | import time 25 | 26 | 27 | def random_bytes(n): 28 | return bytes(os.urandom(n)) 29 | 30 | 31 | def sendbytes(s, a): 32 | s.send(bytes(a)) 33 | 34 | 35 | # valid message types 36 | message_types = [0x00, 0x01, 0x02, 0x03, 0x10, 0x11] 37 | 38 | num_threads = 16 39 | 40 | 41 | def fuzz_any(): 42 | ret = [choice(message_types)] 43 | 44 | ret += os.urandom(128) 45 | return ret 46 | 47 | 48 | def fuzz_singlebyte(): 49 | ret = [choice(message_types)] 50 | return ret 51 | 52 | 53 | def fuzz_assign(): 54 | 55 | ret = [0x10] 56 | 57 | # string: 58 | # 2 bytes len 59 | # n bytes content 60 | 61 | l = randint(0, 255) 62 | ret += [0, l] 63 | 64 | ret += os.urandom(l) 65 | 66 | # byte of type id 67 | ret += [randint(0, 20)] 68 | 69 | # two bytes, entry id 70 | ret += [0, randint(0, 255)] 71 | 72 | # two bytes, sequence 73 | ret += [randint(0, 255), randint(0, 255)] 74 | 75 | # some value 76 | ret += [0, 0] 77 | 78 | return ret 79 | 80 | 81 | def fuzz_update(): 82 | 83 | ret = [0x11] 84 | 85 | # entry id 86 | ret += [0x00, randint(0, 10)] 87 | 88 | # data type 89 | ret += [randint(0, 5)] 90 | 91 | # data 92 | ret += os.urandom(128) 93 | return ret 94 | 95 | 96 | def fuzz_gibberish(): 97 | return os.urandom(128) 98 | 99 | 100 | def fuzz_dumb(): 101 | return [ 102 | randint(0, 5), 103 | randint(0, 5), 104 | randint(0, 5), 105 | randint(0, 5), 106 | randint(0, 5), 107 | randint(0, 5), 108 | randint(0, 5), 109 | ] 110 | 111 | 112 | fuzz_routines = [fuzz_assign, fuzz_singlebyte, fuzz_update, fuzz_gibberish, fuzz_dumb] 113 | 114 | 115 | def fuzz_thread(): 116 | 117 | i = 0 118 | 119 | while True: 120 | 121 | print("Iteration", i) 122 | i += 1 123 | 124 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 125 | s.settimeout(1) 126 | 127 | try: 128 | print("Opening socket") 129 | s.connect((ip, port)) 130 | 131 | sendbytes(s, choice(fuzz_routines)()) 132 | s.recv(1) 133 | 134 | except socket.timeout: 135 | print("Socket timed out, try again") 136 | 137 | except socket.error as e: 138 | if e.errno != errno.ECONNRESET: 139 | raise 140 | finally: 141 | print("Closing socket") 142 | s.close() 143 | 144 | 145 | threads = [threading.Thread(target=fuzz_thread) for i in range(0, num_threads)] 146 | 147 | for t in threads: 148 | t.start() 149 | 150 | time.sleep(0.2) 151 | -------------------------------------------------------------------------------- /tests/test_util.py: -------------------------------------------------------------------------------- 1 | from networktables.util import ntproperty, ChooserControl 2 | import pytest 3 | 4 | 5 | def test_autoupdatevalue(nt): 6 | 7 | # tricksy: make sure that this works *before* initialization 8 | # of network tables happens! 9 | nt.shutdown() 10 | 11 | foo = nt.getGlobalAutoUpdateValue("/SmartDashboard/foo", True, True) 12 | assert foo.value == True 13 | assert foo.get() == True 14 | 15 | nt.startTestMode() 16 | 17 | assert foo.value == True 18 | assert foo.get() == True 19 | 20 | t = nt.getTable("/SmartDashboard") 21 | assert t.getBoolean("foo", None) == True 22 | t.putBoolean("foo", False) 23 | 24 | assert foo.value == False 25 | 26 | 27 | def test_ntproperty(nt, nt_flush): 28 | class Foo(object): 29 | robotTime = ntproperty( 30 | "/SmartDashboard/robotTime", 0, writeDefault=False, inst=nt 31 | ) 32 | dsTime = ntproperty("/SmartDashboard/dsTime", 0, writeDefault=True, inst=nt) 33 | testArray = ntproperty( 34 | "/SmartDashboard/testArray", [1, 2, 3], writeDefault=True, inst=nt 35 | ) 36 | 37 | f = Foo() 38 | 39 | t = nt.getTable("/SmartDashboard") 40 | 41 | assert f.robotTime == 0 42 | assert t.getNumber("robotTime", None) == 0 43 | 44 | f.robotTime = 2 45 | assert t.getNumber("robotTime", None) == 2 46 | 47 | t.putNumber("robotTime", 4) 48 | assert f.robotTime == 4 49 | 50 | assert f.testArray == (1, 2, 3) 51 | f.testArray = [4, 5, 6] 52 | assert f.testArray == (4, 5, 6) 53 | 54 | 55 | def test_ntproperty_emptyarray(nt): 56 | with pytest.raises(TypeError): 57 | 58 | class Foo1(object): 59 | testArray = ntproperty( 60 | "/SmartDashboard/testArray", [], writeDefault=True, inst=nt 61 | ) 62 | 63 | with pytest.raises(TypeError): 64 | 65 | class Foo2(object): 66 | testArray = ntproperty( 67 | "/SmartDashboard/testArray", [], writeDefault=False, inst=nt 68 | ) 69 | 70 | 71 | def test_ntproperty_multitest(nt): 72 | """ 73 | Checks to see that ntproperties still work between NT restarts 74 | """ 75 | 76 | class Foo(object): 77 | robotTime = ntproperty( 78 | "/SmartDashboard/robotTime", 0, writeDefault=False, inst=nt 79 | ) 80 | dsTime = ntproperty("/SmartDashboard/dsTime", 0, writeDefault=True, inst=nt) 81 | 82 | for i in range(3): 83 | print("Iteration", i) 84 | 85 | f = Foo() 86 | 87 | t = nt.getTable("/SmartDashboard") 88 | 89 | assert f.robotTime == 0 90 | assert f.dsTime == 0 91 | 92 | assert t.getNumber("robotTime", None) == 0 93 | assert t.getNumber("dsTime", None) == 0 94 | 95 | f.robotTime = 2 96 | assert t.getNumber("robotTime", None) == 2 97 | assert t.getNumber("dsTime", None) == 0 98 | 99 | t.putNumber("robotTime", 4) 100 | assert f.robotTime == 4 101 | assert f.dsTime == 0 102 | 103 | nt.shutdown() 104 | nt.startTestMode() 105 | 106 | 107 | def test_chooser_control(nt): 108 | 109 | c = ChooserControl("Autonomous Mode", inst=nt) 110 | 111 | assert c.getChoices() == () 112 | assert c.getSelected() is None 113 | 114 | c.setSelected("foo") 115 | assert c.getSelected() == "foo" 116 | 117 | t = nt.getTable("/SmartDashboard/Autonomous Mode") 118 | assert t.getString("selected", None) == "foo" 119 | 120 | t.putStringArray("options", ("option1", "option2")) 121 | assert c.getChoices() == ("option1", "option2") 122 | -------------------------------------------------------------------------------- /_pynetworktables/_impl/storage_save.py: -------------------------------------------------------------------------------- 1 | # validated: 2018-11-27 DS a2ecb1027a62 cpp/Storage_save.cpp 2 | # ---------------------------------------------------------------------------- 3 | # Copyright (c) FIRST 2017. All Rights Reserved. 4 | # Open Source Software - may be modified and shared by FRC teams. The code 5 | # must be accompanied by the FIRST BSD license file in the root directory of 6 | # the project. 7 | # ---------------------------------------------------------------------------- 8 | 9 | import ast 10 | import base64 11 | import re 12 | from configparser import RawConfigParser 13 | 14 | from .constants import ( 15 | NT_BOOLEAN, 16 | NT_DOUBLE, 17 | NT_STRING, 18 | NT_RAW, 19 | NT_BOOLEAN_ARRAY, 20 | NT_DOUBLE_ARRAY, 21 | NT_STRING_ARRAY, 22 | ) 23 | 24 | import logging 25 | 26 | logger = logging.getLogger("nt") 27 | 28 | 29 | PERSISTENT_SECTION = "NetworkTables Storage 3.0" 30 | 31 | _key_bool = re.compile('boolean "(.+)"') 32 | _key_double = re.compile('double "(.+)"') 33 | _key_string = re.compile('string "(.+)"') 34 | _key_raw = re.compile('raw "(.+)"') 35 | _key_bool_array = re.compile('array boolean "(.+)"') 36 | _key_double_array = re.compile('array double "(.+)"') 37 | _key_string_array = re.compile('array string "(.+)"') 38 | 39 | _value_string = re.compile(r'"((?:\\.|[^"\\])*)",?') 40 | 41 | # TODO: these escape functions almost certainly don't deal with unicode 42 | # correctly 43 | 44 | # TODO: strictly speaking, this isn't 100% compatible with ntcore... but 45 | 46 | 47 | def _unescape_string(s): 48 | # shortcut if no escapes present 49 | if "\\" not in s: 50 | return s 51 | 52 | # let python do the hard work 53 | return ast.literal_eval('"%s"' % s) 54 | 55 | 56 | # This is mostly what we want... unicode strings won't work properly though 57 | _table = {i: chr(i) if i >= 32 and i < 127 else "\\x%02x" % i for i in range(256)} 58 | _table[ord('"')] = '\\"' 59 | _table[ord("\\")] = "\\\\" 60 | _table[ord("\n")] = "\\n" 61 | _table[ord("\t")] = "\\t" 62 | _table[ord("\r")] = "\\r" 63 | 64 | 65 | def _escape_string(s): 66 | return s.translate(_table) 67 | 68 | 69 | def save_entries(fp, entries): 70 | 71 | parser = RawConfigParser() 72 | parser.optionxform = str 73 | parser.add_section(PERSISTENT_SECTION) 74 | 75 | for name, value in entries: 76 | if not value: 77 | continue 78 | 79 | t = value.type 80 | v = value.value 81 | 82 | if t == NT_BOOLEAN: 83 | name = 'boolean "%s"' % _escape_string(name) 84 | vrepr = "true" if v else "false" 85 | elif t == NT_DOUBLE: 86 | name = 'double "%s"' % _escape_string(name) 87 | vrepr = str(v) 88 | elif t == NT_STRING: 89 | name = 'string "%s"' % _escape_string(name) 90 | vrepr = '"%s"' % _escape_string(v) 91 | elif t == NT_RAW: 92 | name = 'raw "%s"' % _escape_string(name) 93 | vrepr = base64.b64encode(v).decode("ascii") 94 | elif t == NT_BOOLEAN_ARRAY: 95 | name = 'array boolean "%s"' % _escape_string(name) 96 | vrepr = ",".join(["true" if vv else "false" for vv in v]) 97 | elif t == NT_DOUBLE_ARRAY: 98 | name = 'array double "%s"' % _escape_string(name) 99 | vrepr = ",".join([str(vv) for vv in v]) 100 | elif t == NT_STRING_ARRAY: 101 | name = 'array string "%s"' % _escape_string(name) 102 | vrepr = '","'.join([_escape_string(vv) for vv in v]) 103 | if vrepr: 104 | vrepr = '"%s"' % vrepr 105 | else: 106 | continue 107 | 108 | parser.set(PERSISTENT_SECTION, name, vrepr) 109 | 110 | parser.write(fp, space_around_delimiters=False) 111 | -------------------------------------------------------------------------------- /_pynetworktables/_impl/value.py: -------------------------------------------------------------------------------- 1 | # validated: 2018-11-27 DS 175c6c1f0130 cpp/Value.cpp include/networktables/NetworkTableValue.h 2 | """ 3 | Internal storage for ntcore values 4 | 5 | Uses namedtuple for efficiency, and because Value objects are supposed 6 | to be immutable. Will have to measure that and see if there's a performance 7 | penalty for this in python. 8 | 9 | Original ntcore stores the last change time, but it doesn't seem to 10 | be used anywhere, so we don't store that to make equality comparison 11 | more efficient. 12 | """ 13 | 14 | from collections import namedtuple 15 | from .constants import ( 16 | NT_BOOLEAN, 17 | NT_DOUBLE, 18 | NT_STRING, 19 | NT_RAW, 20 | NT_BOOLEAN_ARRAY, 21 | NT_DOUBLE_ARRAY, 22 | NT_STRING_ARRAY, 23 | NT_RPC, 24 | ) 25 | 26 | 27 | class Value(namedtuple("Value", ["type", "value"])): 28 | __slots__ = () 29 | 30 | @classmethod 31 | def makeBoolean(cls, value): 32 | if value: 33 | return cls._TRUE_VALUE 34 | else: 35 | return cls._FALSE_VALUE 36 | 37 | @classmethod 38 | def makeDouble(cls, value): 39 | return cls(NT_DOUBLE, float(value)) 40 | 41 | @classmethod 42 | def makeString(cls, value): 43 | return cls(NT_STRING, str(value)) 44 | 45 | @classmethod 46 | def makeRaw(cls, value): 47 | return cls(NT_RAW, bytes(value)) 48 | 49 | # TODO: array stuff a good idea? 50 | 51 | @classmethod 52 | def makeBooleanArray(cls, value): 53 | return cls(NT_BOOLEAN_ARRAY, tuple(bool(v) for v in value)) 54 | 55 | @classmethod 56 | def makeDoubleArray(cls, value): 57 | return cls(NT_DOUBLE_ARRAY, tuple(float(v) for v in value)) 58 | 59 | @classmethod 60 | def makeStringArray(cls, value): 61 | return cls(NT_STRING_ARRAY, tuple(str(v) for v in value)) 62 | 63 | @classmethod 64 | def makeRpc(cls, value): 65 | return cls(NT_RPC, str(value)) 66 | 67 | @classmethod 68 | def getFactory(cls, value): 69 | if isinstance(value, bool): 70 | return cls.makeBoolean 71 | elif isinstance(value, (int, float)): 72 | return cls.makeDouble 73 | elif isinstance(value, str): 74 | return cls.makeString 75 | elif isinstance(value, (bytes, bytearray)): 76 | return cls.makeRaw 77 | 78 | # Do best effort for arrays, but can't catch all cases 79 | # .. if you run into an error here, use a less generic type 80 | elif isinstance(value, (list, tuple)): 81 | if not value: 82 | raise TypeError("If you use a list here, cannot be empty") 83 | 84 | first = value[0] 85 | if isinstance(first, bool): 86 | return cls.makeBooleanArray 87 | elif isinstance(first, (int, float)): 88 | return cls.makeDoubleArray 89 | elif isinstance(first, str): 90 | return cls.makeStringArray 91 | else: 92 | raise ValueError("Can only use lists of bool/int/float/strings") 93 | 94 | elif value is None: 95 | raise ValueError("Cannot put None into NetworkTable") 96 | else: 97 | raise ValueError( 98 | "Can only put bool/int/float/str/bytes or lists/tuples of them" 99 | ) 100 | 101 | @classmethod 102 | def getFactoryByType(cls, type_id): 103 | return cls._make_map[type_id] 104 | 105 | 106 | # optimization 107 | Value._TRUE_VALUE = Value(NT_BOOLEAN, True) 108 | Value._FALSE_VALUE = Value(NT_BOOLEAN, False) 109 | 110 | Value._make_map = { 111 | NT_BOOLEAN: Value.makeBoolean, 112 | NT_DOUBLE: Value.makeDouble, 113 | NT_STRING: Value.makeString, 114 | NT_RAW: Value.makeRaw, 115 | NT_BOOLEAN_ARRAY: Value.makeBooleanArray, 116 | NT_DOUBLE_ARRAY: Value.makeDoubleArray, 117 | NT_STRING_ARRAY: Value.makeStringArray, 118 | NT_RPC: Value.makeRpc, 119 | } 120 | -------------------------------------------------------------------------------- /_pynetworktables/_impl/ds_client.py: -------------------------------------------------------------------------------- 1 | # validated: 2018-11-27 DS 18c8cce6a78d cpp/DsClient.cpp cpp/DsClient.h 2 | # ---------------------------------------------------------------------------- 3 | # Copyright (c) FIRST 2017. All Rights Reserved. 4 | # Open Source Software - may be modified and shared by FRC teams. The code 5 | # must be accompanied by the FIRST BSD license file in the root directory of 6 | # the project. 7 | # ---------------------------------------------------------------------------- 8 | 9 | import json 10 | import threading 11 | 12 | from .support.safe_thread import SafeThread 13 | from .tcpsockets.tcp_connector import TcpConnector 14 | 15 | import logging 16 | 17 | logger = logging.getLogger("nt") 18 | 19 | 20 | class DsClient(object): 21 | def __init__(self, dispatcher, verbose=False): 22 | self.m_dispatcher = dispatcher 23 | self.verbose = verbose 24 | 25 | self.m_active = False 26 | self.m_owner = None # type: SafeThread 27 | 28 | self.m_mutex = threading.Lock() 29 | self.m_cond = threading.Condition(self.m_mutex) 30 | 31 | self.m_port = None # type: int 32 | self.m_stream = None 33 | 34 | def start(self, port): 35 | with self.m_mutex: 36 | self.m_port = port 37 | if not self.m_active: 38 | self.m_active = True 39 | self.m_owner = SafeThread(target=self._thread_main, name="nt-dsclient") 40 | 41 | def stop(self): 42 | with self.m_mutex: 43 | # Close the stream so the read (if any) terminates. 44 | self.m_active = False 45 | if self.m_stream: 46 | self.m_stream.close() 47 | self.m_cond.notify() 48 | 49 | def _thread_main(self): 50 | oldip = 0 51 | connector = TcpConnector(verbose=False, timeout=1) 52 | 53 | while self.m_active: 54 | # wait for periodic reconnect or termination 55 | with self.m_mutex: 56 | self.m_cond.wait_for(lambda: not self.m_active, timeout=0.5) 57 | port = self.m_port 58 | 59 | if not self.m_active: 60 | break 61 | 62 | self.m_stream = connector.connect(("127.0.0.1", 1742)) 63 | if not self.m_active: 64 | break 65 | if not self.m_stream: 66 | continue 67 | 68 | while self.m_active and self.m_stream: 69 | json_blob = self.m_stream.readline() 70 | if not json_blob: 71 | # We've reached EOF. 72 | with self.m_mutex: 73 | self.m_stream.close() 74 | self.m_stream = None 75 | 76 | if not self.m_active: 77 | break 78 | 79 | try: 80 | obj = json.loads(json_blob.decode()) 81 | except (json.JSONDecodeError, UnicodeDecodeError): 82 | continue 83 | try: 84 | ip = int(obj["robotIP"]) 85 | except (KeyError, ValueError): 86 | continue 87 | 88 | # If zero, clear the server override 89 | if ip == 0: 90 | self.m_dispatcher.clearServerOverride() 91 | oldip = 0 92 | continue 93 | 94 | # If unchanged, don't reconnect 95 | if ip == oldip: 96 | continue 97 | oldip = ip 98 | 99 | # Convert number into dotted quad 100 | ip_str = "%d.%d.%d.%d" % ( 101 | (ip >> 24) & 0xFF, 102 | (ip >> 16) & 0xFF, 103 | (ip >> 8) & 0xFF, 104 | ip & 0xFF, 105 | ) 106 | if self.verbose: 107 | logger.info("client: DS overriding server IP to %s", ip_str) 108 | self.m_dispatcher.setServerOverride(ip_str, port) 109 | 110 | # We disconnected from the DS, clear the server override 111 | self.m_dispatcher.clearServerOverride() 112 | oldip = 0 113 | 114 | # Python note: we don't call Dispatcher.clearServerOverride() again. 115 | # Either it was already called, or we were never active. 116 | -------------------------------------------------------------------------------- /_pynetworktables/_impl/entry_notifier.py: -------------------------------------------------------------------------------- 1 | # validated: 2017-10-01 DS e4a8bff70e77 cpp/EntryNotifier.cpp cpp/EntryNotifier.h cpp/IEntryNotifier.h 2 | # ---------------------------------------------------------------------------- 3 | # Copyright (c) FIRST 2017. All Rights Reserved. 4 | # Open Source Software - may be modified and shared by FRC teams. The code 5 | # must be accompanied by the FIRST BSD license file in the root directory of 6 | # the project. 7 | # ---------------------------------------------------------------------------- 8 | 9 | from collections import namedtuple 10 | 11 | from .callback_manager import CallbackManager, CallbackThread 12 | 13 | from .constants import ( 14 | NT_NOTIFY_IMMEDIATE, 15 | NT_NOTIFY_LOCAL, 16 | NT_NOTIFY_UPDATE, 17 | NT_NOTIFY_FLAGS, 18 | ) 19 | 20 | 21 | _EntryListenerData = namedtuple( 22 | "EntryListenerData", 23 | [ 24 | "prefix", 25 | "local_id", # we don't have entry handles like ntcore has 26 | "flags", 27 | "callback", 28 | "poller_uid", 29 | ], 30 | ) 31 | 32 | # 33 | _EntryNotification = namedtuple( 34 | "EntryNotification", ["name", "value", "flags", "local_id"] 35 | ) 36 | 37 | _assign_both = NT_NOTIFY_UPDATE | NT_NOTIFY_FLAGS 38 | _immediate_local = NT_NOTIFY_IMMEDIATE | NT_NOTIFY_LOCAL 39 | 40 | 41 | class EntryNotifierThread(CallbackThread): 42 | def __init__(self): 43 | CallbackThread.__init__(self, "entry-notifier") 44 | 45 | def matches(self, listener, data): 46 | if not data.value: 47 | return False 48 | 49 | # must match local id or prefix 50 | # -> python-specific: match this first, since it's the most likely thing 51 | # to not match 52 | if listener.local_id is not None: 53 | if listener.local_id != data.local_id: 54 | return False 55 | else: 56 | if not data.name.startswith(listener.prefix): 57 | return False 58 | 59 | # Flags must be within requested flag set for this listener. 60 | # Because assign messages can result in both a value and flags update, 61 | # we handle that case specially. 62 | listen_flags = listener.flags & ~_immediate_local 63 | flags = data.flags & ~_immediate_local 64 | 65 | if (flags & _assign_both) == _assign_both: 66 | if (listen_flags & _assign_both) == 0: 67 | return False 68 | listen_flags &= ~_assign_both 69 | flags &= ~_assign_both 70 | 71 | if (flags & ~listen_flags) != 0: 72 | return False 73 | 74 | return True 75 | 76 | def setListener(self, data, listener_uid): 77 | pass 78 | 79 | def doCallback(self, callback, data): 80 | callback(data) 81 | 82 | 83 | class EntryNotifier(CallbackManager): 84 | 85 | THREAD_CLASS = EntryNotifierThread 86 | 87 | def __init__(self, verbose): 88 | CallbackManager.__init__(self, verbose) 89 | 90 | self.m_local_notifiers = False 91 | 92 | def add(self, callback, prefix, flags): 93 | if (flags & NT_NOTIFY_LOCAL) != 0: 94 | self.m_local_notifiers = True 95 | return self.doAdd(_EntryListenerData(prefix, None, flags, callback, None)) 96 | 97 | def addById(self, callback, local_id, flags): 98 | if (flags & NT_NOTIFY_LOCAL) != 0: 99 | self.m_local_notifiers = True 100 | return self.doAdd(_EntryListenerData(None, local_id, flags, callback, None)) 101 | 102 | def addPolled(self, poller_uid, prefix, flags): 103 | if (flags & NT_NOTIFY_LOCAL) != 0: 104 | self.m_local_notifiers = True 105 | return self.doAdd(_EntryListenerData(prefix, None, flags, None, poller_uid)) 106 | 107 | def addPolledById(self, poller_uid, local_id, flags): 108 | if (flags & NT_NOTIFY_LOCAL) != 0: 109 | self.m_local_notifiers = True 110 | return self.doAdd(_EntryListenerData(None, local_id, flags, None, poller_uid)) 111 | 112 | def notifyEntry(self, local_id, name, value, flags, only_listener=None): 113 | 114 | # optimization: don't generate needless local queue entries if we have 115 | # no local listeners (as this is a common case on the server side) 116 | if not self.m_local_notifiers and (flags & NT_NOTIFY_LOCAL) != 0: 117 | return 118 | 119 | self.send(only_listener, _EntryNotification(name, value, flags, local_id)) 120 | 121 | def start(self): 122 | CallbackManager.start(self) 123 | -------------------------------------------------------------------------------- /_pynetworktables/_impl/support/_impl_debug.py: -------------------------------------------------------------------------------- 1 | # notrack 2 | """ 3 | Instrumentation for finding deadlocks in networktables 4 | """ 5 | 6 | from __future__ import print_function 7 | 8 | import inspect 9 | import socket 10 | import threading 11 | import time 12 | 13 | # Number of seconds to block 14 | sock_block_period = None 15 | 16 | # List of locks that can be acquired from the main thread 17 | main_locks = ["entry_lock", "trans_lock"] 18 | 19 | # List of locks that are allowed to be held when accessing a socket 20 | # -> must never be locks that can be acquired by the main thread 21 | sock_locks = ["client_conn_lock", "server_conn_lock", "write_lock"] 22 | 23 | # Dictionary of locks 24 | # key: name, value: locks that can be held when acquiring the lock 25 | locks = { 26 | # Never held by robot thread 27 | "client_conn_lock": ["client_conn_lock"], 28 | "entry_lock": ["entry_lock", "client_conn_lock"], 29 | # Never held by robot thread 30 | "server_conn_lock": ["server_conn_lock"], 31 | "trans_lock": [ 32 | "entry_lock", 33 | # Not 100% sure if this should be allowed 34 | # -> this only happens when NetworkTable API calls are made from 35 | # a fired listener 36 | "server_conn_lock", 37 | "client_conn_lock", 38 | ], 39 | # Never held by robot thread 40 | "write_lock": ["client_conn_lock", "server_conn_lock", "write_lock"], 41 | } 42 | 43 | local = threading.local() 44 | 45 | 46 | class WrappedLock(threading._PyRLock): 47 | def __init__(self, name): 48 | threading._PyRLock.__init__(self) 49 | self._name = name 50 | self._nt_creator = _get_caller() 51 | 52 | def acquire(self, blocking=True, timeout=-1): 53 | 54 | # This check isn't strictly true.. 55 | if isinstance(threading.current_thread(), threading._MainThread): 56 | assert self._name in main_locks, ( 57 | "%s cannot be held in main thread" % self._name 58 | ) 59 | 60 | if not hasattr(local, "held_locks"): 61 | local.held_locks = [] 62 | 63 | for lock in local.held_locks: 64 | assert ( 65 | lock in locks[self._name] 66 | ), "Cannot hold %s when trying to acquire %s" % (lock._name, self._name) 67 | 68 | retval = threading._PyRLock.acquire(self, blocking=blocking, timeout=timeout) 69 | if retval != False: 70 | local.held_locks.append(self) 71 | 72 | __enter__ = acquire 73 | 74 | def release(self): 75 | threading._PyRLock.release(self) 76 | assert local.held_locks[-1] == self 77 | local.held_locks.pop() 78 | 79 | # Allow this to be used in comparisons 80 | 81 | def __eq__(self, other): 82 | if isinstance(other, str): 83 | return self._name.__eq__(other) 84 | else: 85 | return self._name.__eq__(other._name) 86 | 87 | def __cmp__(self, other): 88 | if isinstance(other, str): 89 | return self._name.__cmp__(other) 90 | else: 91 | return self._name.__cmp__(other._name) 92 | 93 | def __hash__(self): 94 | return self._name.__hash__() 95 | 96 | 97 | def create_tracked_rlock(name): 98 | assert name in locks 99 | return WrappedLock(name) 100 | 101 | 102 | def assert_not_locked(t): 103 | 104 | assert not isinstance( 105 | threading.current_thread(), threading._MainThread 106 | ), "Should not make socket calls from main thread" 107 | 108 | if not hasattr(local, "held_locks"): 109 | local.held_locks = [] 110 | 111 | for lock in local.held_locks: 112 | assert lock in sock_locks, "ERROR: network %s was made while holding %s" % ( 113 | t, 114 | lock._name, 115 | ) 116 | 117 | 118 | class WrappedFile: 119 | def __init__(self, file): 120 | self._file = file 121 | 122 | def write(self, data): 123 | assert_not_locked("write") 124 | if sock_block_period: 125 | time.sleep(sock_block_period) 126 | return self._file.write(data) 127 | 128 | def read(self, *args, **kwargs): 129 | assert_not_locked("read") 130 | if sock_block_period: 131 | time.sleep(sock_block_period) 132 | return self._file.read(*args, **kwargs) 133 | 134 | def __getattr__(self, attr): 135 | return getattr(self._file, attr) 136 | 137 | 138 | def blocking_sock_makefile(s, mode): 139 | return WrappedFile(s.makefile(mode)) 140 | 141 | 142 | def blocking_sock_create_connection(address): 143 | assert_not_locked("connect") 144 | if sock_block_period: 145 | time.sleep(sock_block_period) 146 | return socket.create_connection(address) 147 | 148 | 149 | def _get_caller(): 150 | curframe = inspect.currentframe() 151 | calframe = inspect.getouterframes(curframe, 3) 152 | return "%s:%s %s" % (calframe[3][1], calframe[3][2], calframe[3][3]) 153 | -------------------------------------------------------------------------------- /tests/test_value.py: -------------------------------------------------------------------------------- 1 | # ---------------------------------------------------------------------------- 2 | # Copyright (c) FIRST 2017. All Rights Reserved. 3 | # Open Source Software - may be modified and shared by FRC teams. The code 4 | # must be accompanied by the FIRST BSD license file in the root directory of 5 | # the project. 6 | # ---------------------------------------------------------------------------- 7 | 8 | # 9 | # These tests are adapted from ntcore's test suite 10 | # 11 | 12 | from _pynetworktables._impl.constants import ( 13 | NT_BOOLEAN, 14 | NT_DOUBLE, 15 | NT_STRING, 16 | NT_RAW, 17 | NT_BOOLEAN_ARRAY, 18 | NT_DOUBLE_ARRAY, 19 | NT_STRING_ARRAY, 20 | ) 21 | from _pynetworktables._impl.value import Value 22 | 23 | 24 | def test_Boolean(): 25 | v = Value.makeBoolean(False) 26 | assert NT_BOOLEAN == v.type 27 | assert not v.value 28 | 29 | v = Value.makeBoolean(True) 30 | assert NT_BOOLEAN == v.type 31 | assert v.value 32 | 33 | 34 | def test_Double(): 35 | v = Value.makeDouble(0.5) 36 | assert NT_DOUBLE == v.type 37 | assert 0.5 == v.value 38 | 39 | v = Value.makeDouble(0.25) 40 | assert NT_DOUBLE == v.type 41 | assert 0.25 == v.value 42 | 43 | 44 | def test_String(): 45 | v = Value.makeString("hello") 46 | assert NT_STRING == v.type 47 | assert "hello" == v.value 48 | 49 | v = Value.makeString("goodbye") 50 | assert NT_STRING == v.type 51 | assert "goodbye" == v.value 52 | 53 | 54 | def test_Raw(): 55 | v = Value.makeRaw(b"hello") 56 | assert NT_RAW == v.type 57 | assert b"hello" == v.value 58 | 59 | v = Value.makeRaw(b"goodbye") 60 | assert NT_RAW == v.type 61 | assert b"goodbye" == v.value 62 | 63 | 64 | def test_BooleanArray(): 65 | v = Value.makeBooleanArray([True, False, True]) 66 | assert NT_BOOLEAN_ARRAY == v.type 67 | assert (True, False, True) == v.value 68 | 69 | 70 | def test_DoubleArray(): 71 | v = Value.makeDoubleArray([0.5, 0.25, 0.5]) 72 | assert NT_DOUBLE_ARRAY == v.type 73 | assert (0.5, 0.25, 0.5) == v.value 74 | 75 | 76 | def test_StringArray(): 77 | v = Value.makeStringArray(["hello", "goodbye", "string"]) 78 | assert NT_STRING_ARRAY == v.type 79 | assert ("hello", "goodbye", "string") == v.value 80 | 81 | 82 | def test_MixedComparison(): 83 | 84 | v2 = Value.makeBoolean(True) 85 | v3 = Value.makeDouble(0.5) 86 | assert v2 != v3 # boolean vs double 87 | 88 | 89 | def test_BooleanComparison(): 90 | v1 = Value.makeBoolean(True) 91 | v2 = Value.makeBoolean(True) 92 | assert v1 == v2 93 | v2 = Value.makeBoolean(False) 94 | assert v1 != v2 95 | 96 | 97 | def test_DoubleComparison(): 98 | v1 = Value.makeDouble(0.25) 99 | v2 = Value.makeDouble(0.25) 100 | assert v1 == v2 101 | v2 = Value.makeDouble(0.5) 102 | assert v1 != v2 103 | 104 | 105 | def test_StringComparison(): 106 | v1 = Value.makeString("hello") 107 | v2 = Value.makeString("hello") 108 | assert v1 == v2 109 | v2 = Value.makeString("world") 110 | # different contents 111 | assert v1 != v2 112 | v2 = Value.makeString("goodbye") 113 | # different size 114 | assert v1 != v2 115 | 116 | 117 | def test_BooleanArrayComparison(): 118 | v1 = Value.makeBooleanArray([1, 0, 1]) 119 | v2 = Value.makeBooleanArray((1, 0, 1)) 120 | assert v1 == v2 121 | 122 | # different contents 123 | v2 = Value.makeBooleanArray([1, 1, 1]) 124 | assert v1 != v2 125 | 126 | # different size 127 | v2 = Value.makeBooleanArray([True, False]) 128 | assert v1 != v2 129 | 130 | 131 | def test_DoubleArrayComparison(): 132 | v1 = Value.makeDoubleArray([0.5, 0.25, 0.5]) 133 | v2 = Value.makeDoubleArray((0.5, 0.25, 0.5)) 134 | assert v1 == v2 135 | 136 | # different contents 137 | v2 = Value.makeDoubleArray([0.5, 0.5, 0.5]) 138 | assert v1 != v2 139 | 140 | # different size 141 | v2 = Value.makeDoubleArray([0.5, 0.25]) 142 | assert v1 != v2 143 | 144 | 145 | def test_StringArrayComparison(): 146 | v1 = Value.makeStringArray(["hello", "goodbye", "string"]) 147 | v2 = Value.makeStringArray(("hello", "goodbye", "string")) 148 | assert v1 == v2 149 | 150 | # different contents 151 | v2 = Value.makeStringArray(["hello", "goodby2", "string"]) 152 | assert v1 != v2 153 | 154 | # different sized contents 155 | v2 = Value.makeStringArray(["hello", "goodbye2", "string"]) 156 | assert v1 != v2 157 | 158 | # different size 159 | v2 = Value.makeStringArray(["hello", "goodbye"]) 160 | assert v1 != v2 161 | 162 | 163 | # 164 | # Additional Python tests 165 | # 166 | 167 | 168 | def test_unicode(): 169 | # copyright symbol 170 | v1 = Value.makeString(u"\xA9") 171 | assert v1.value == u"\xA9" 172 | 173 | 174 | def test_bytearray(): 175 | v1 = Value.makeRaw(bytearray(b"\x01\x02\x00")) 176 | assert v1.type == NT_RAW 177 | assert v1.value == bytearray(b"\x01\x02\x00") 178 | assert v1.value == b"\x01\x02\x00" 179 | -------------------------------------------------------------------------------- /tests/test_entry_listener.py: -------------------------------------------------------------------------------- 1 | # ---------------------------------------------------------------------------- 2 | # Copyright (c) FIRST 2017. All Rights Reserved. 3 | # Open Source Software - may be modified and shared by FRC teams. The code 4 | # must be accompanied by the FIRST BSD license file in the root directory of 5 | # the project. 6 | # ---------------------------------------------------------------------------- 7 | 8 | # 9 | # These tests are adapted from ntcore's test suite 10 | # 11 | 12 | import pytest 13 | 14 | from threading import Condition 15 | 16 | from _pynetworktables._impl.constants import NT_NOTIFY_LOCAL, NT_NOTIFY_NEW 17 | 18 | from _pynetworktables._impl.value import Value 19 | 20 | 21 | class SC(object): 22 | def __init__(self): 23 | self.events = [] 24 | self.event_cond = Condition() 25 | 26 | def __call__(self, event): 27 | with self.event_cond: 28 | self.events.append(event) 29 | self.event_cond.notify() 30 | 31 | def wait(self, count): 32 | with self.event_cond: 33 | result = self.event_cond.wait_for(lambda: len(self.events) == count, 2) 34 | assert result, "expected %s events, got %s" % (count, len(self.events)) 35 | return self.events[:] 36 | 37 | 38 | @pytest.fixture 39 | def server_cb(): 40 | return SC() 41 | 42 | 43 | def test_EntryNewLocal(nt_live, server_cb): 44 | nt_server, nt_client = nt_live 45 | nt_server_api = nt_server._api 46 | 47 | nt_server_api.addEntryListenerById( 48 | nt_server_api.getEntryId("/foo"), server_cb, NT_NOTIFY_NEW | NT_NOTIFY_LOCAL 49 | ) 50 | 51 | # Trigger an event 52 | nt_server_api.setEntryValueById( 53 | nt_server_api.getEntryId("/foo/bar"), Value.makeDouble(2.0) 54 | ) 55 | nt_server_api.setEntryValueById( 56 | nt_server_api.getEntryId("/foo"), Value.makeDouble(1.0) 57 | ) 58 | 59 | assert nt_server_api.waitForEntryListenerQueue(1.0) 60 | 61 | # Check the event 62 | events = server_cb.wait(1) 63 | 64 | # assert events[0].listener == handle 65 | assert events[0].local_id == nt_server_api.getEntryId("/foo") 66 | assert events[0].name == "/foo" 67 | assert events[0].value == Value.makeDouble(1.0) 68 | assert events[0].flags == NT_NOTIFY_NEW | NT_NOTIFY_LOCAL 69 | 70 | 71 | def test_EntryNewRemote(nt_live, server_cb): 72 | nt_server, nt_client = nt_live 73 | nt_server_api = nt_server._api 74 | nt_client_api = nt_client._api 75 | 76 | nt_server_api.addEntryListenerById( 77 | nt_server_api.getEntryId("/foo"), server_cb, NT_NOTIFY_NEW 78 | ) 79 | 80 | # Trigger an event 81 | nt_client_api.setEntryValueById( 82 | nt_client_api.getEntryId("/foo/bar"), Value.makeDouble(2.0) 83 | ) 84 | nt_client_api.setEntryValueById( 85 | nt_client_api.getEntryId("/foo"), Value.makeDouble(1.0) 86 | ) 87 | 88 | nt_client_api.flush() 89 | 90 | assert nt_server_api.waitForEntryListenerQueue(1.0) 91 | 92 | # Check the event 93 | events = server_cb.wait(1) 94 | 95 | # assert events[0].listener == handle 96 | assert events[0].local_id == nt_server_api.getEntryId("/foo") 97 | assert events[0].name == "/foo" 98 | assert events[0].value == Value.makeDouble(1.0) 99 | assert events[0].flags == NT_NOTIFY_NEW 100 | 101 | 102 | def test_PrefixNewLocal(nt_live, server_cb): 103 | nt_server, nt_client = nt_live 104 | nt_server_api = nt_server._api 105 | 106 | nt_server_api.addEntryListener("/foo", server_cb, NT_NOTIFY_NEW | NT_NOTIFY_LOCAL) 107 | 108 | # Trigger an event 109 | nt_server_api.setEntryValueById( 110 | nt_server_api.getEntryId("/foo/bar"), Value.makeDouble(1.0) 111 | ) 112 | nt_server_api.setEntryValueById( 113 | nt_server_api.getEntryId("/baz"), Value.makeDouble(1.0) 114 | ) 115 | 116 | assert nt_server_api.waitForEntryListenerQueue(1.0) 117 | 118 | events = server_cb.wait(1) 119 | 120 | # assert events[0].listener == handle 121 | assert events[0].local_id == nt_server_api.getEntryId("/foo/bar") 122 | assert events[0].name == "/foo/bar" 123 | assert events[0].value == Value.makeDouble(1.0) 124 | assert events[0].flags == NT_NOTIFY_NEW | NT_NOTIFY_LOCAL 125 | 126 | 127 | def test_PrefixNewRemote(nt_live, server_cb): 128 | nt_server, nt_client = nt_live 129 | nt_server_api = nt_server._api 130 | nt_client_api = nt_client._api 131 | 132 | nt_server_api.addEntryListener("/foo", server_cb, NT_NOTIFY_NEW | NT_NOTIFY_LOCAL) 133 | 134 | # Trigger an event 135 | nt_client_api.setEntryValueById( 136 | nt_client_api.getEntryId("/foo/bar"), Value.makeDouble(1.0) 137 | ) 138 | nt_client_api.setEntryValueById( 139 | nt_client_api.getEntryId("/baz"), Value.makeDouble(1.0) 140 | ) 141 | 142 | assert nt_server_api.waitForEntryListenerQueue(1.0) 143 | 144 | # Check the event 145 | events = server_cb.wait(1) 146 | 147 | # assert events[0].listener == handle 148 | assert events[0].local_id == nt_server_api.getEntryId("/foo/bar") 149 | assert events[0].name == "/foo/bar" 150 | assert events[0].value == Value.makeDouble(1.0) 151 | assert events[0].flags == NT_NOTIFY_NEW 152 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys 5 | import os 6 | 7 | from os.path import abspath, join, dirname 8 | 9 | sys.path.insert(0, abspath(dirname(__file__))) 10 | sys.path.insert(0, abspath(join(dirname(__file__), ".."))) 11 | 12 | import networktables 13 | 14 | # -- RTD configuration ------------------------------------------------ 15 | 16 | # on_rtd is whether we are on readthedocs.org, this line of code grabbed from docs.readthedocs.org 17 | on_rtd = os.environ.get("READTHEDOCS", None) == "True" 18 | 19 | # This is used for linking and such so we link to the thing we're building 20 | rtd_version = os.environ.get("READTHEDOCS_VERSION", "latest") 21 | if rtd_version not in ["stable", "latest"]: 22 | rtd_version = "stable" 23 | 24 | # -- General configuration ------------------------------------------------ 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be 27 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 28 | # ones. 29 | extensions = [ 30 | "sphinx.ext.autodoc", 31 | "sphinx.ext.viewcode", 32 | "sphinx.ext.intersphinx", 33 | "sphinx_autodoc_typehints", 34 | ] 35 | 36 | intersphinx_mapping = { 37 | "robotpy": ("http://robotpy.readthedocs.io/en/%s/" % rtd_version, None), 38 | "wpilib": ("http://robotpy-wpilib.readthedocs.io/en/%s/" % rtd_version, None), 39 | } 40 | 41 | # Add any paths that contain templates here, relative to this directory. 42 | templates_path = ["_templates"] 43 | 44 | # The suffix of source filenames. 45 | source_suffix = ".rst" 46 | 47 | # The master toctree document. 48 | master_doc = "index" 49 | 50 | # General information about the project. 51 | project = "RobotPy NetworkTables" 52 | copyright = "2016, RobotPy Development Team" 53 | 54 | 55 | # The version info for the project you're documenting, acts as replacement for 56 | # |version| and |release|, also used in various other places throughout the 57 | # built documents. 58 | # 59 | # The short X.Y version. 60 | version = ".".join(networktables.__version__.split(".")[:2]) 61 | # The full version, including alpha/beta/rc tags. 62 | release = networktables.__version__ 63 | 64 | autoclass_content = "both" 65 | 66 | # List of patterns, relative to source directory, that match files and 67 | # directories to ignore when looking for source files. 68 | exclude_patterns = ["_build"] 69 | 70 | # The name of the Pygments (syntax highlighting) style to use. 71 | pygments_style = "sphinx" 72 | 73 | # -- Options for HTML output ---------------------------------------------- 74 | 75 | # The theme to use for HTML and HTML Help pages. See the documentation for 76 | # a list of builtin themes. 77 | html_theme = "default" 78 | 79 | # on_rtd is whether we are on readthedocs.org, this line of code grabbed from docs.readthedocs.org 80 | on_rtd = os.environ.get("READTHEDOCS", None) == "True" 81 | 82 | if not on_rtd: # only import and set the theme if we're building docs locally 83 | import sphinx_rtd_theme 84 | 85 | html_theme = "sphinx_rtd_theme" 86 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 87 | else: 88 | html_theme = "default" 89 | 90 | # Output file base name for HTML help builder. 91 | htmlhelp_basename = "networktablesdoc" 92 | 93 | 94 | # -- Options for LaTeX output --------------------------------------------- 95 | 96 | latex_elements = {} 97 | 98 | # Grouping the document tree into LaTeX files. List of tuples 99 | # (source start file, target name, title, 100 | # author, documentclass [howto, manual, or own class]). 101 | latex_documents = [ 102 | ( 103 | "index", 104 | "networktables.tex", 105 | "RobotPy networktables Documentation", 106 | "RobotPy Development Team", 107 | "manual", 108 | ) 109 | ] 110 | 111 | # -- Options for manual page output --------------------------------------- 112 | 113 | # One entry per manual page. List of tuples 114 | # (source start file, name, description, authors, manual section). 115 | man_pages = [ 116 | ( 117 | "index", 118 | "networktables", 119 | "RobotPy networktables Documentation", 120 | ["RobotPy Development Team"], 121 | 1, 122 | ) 123 | ] 124 | 125 | # -- Options for Texinfo output ------------------------------------------- 126 | 127 | # Grouping the document tree into Texinfo files. List of tuples 128 | # (source start file, target name, title, author, 129 | # dir menu entry, description, category) 130 | texinfo_documents = [ 131 | ( 132 | "index", 133 | "networktables", 134 | "RobotPy networktables Documentation", 135 | "RobotPy Development Team", 136 | "networktables", 137 | "One line description of project.", 138 | "Miscellaneous", 139 | ) 140 | ] 141 | 142 | # -- Options for Epub output ---------------------------------------------- 143 | 144 | # Bibliographic Dublin Core info. 145 | epub_title = "RobotPy NetworkTables" 146 | epub_author = "RobotPy Development Team" 147 | epub_publisher = "RobotPy Development Team" 148 | epub_copyright = "2014, RobotPy Development Team" 149 | 150 | # A list of files that should not be packed into the epub file. 151 | epub_exclude_files = ["search.html"] 152 | 153 | # -- Custom Document processing ---------------------------------------------- 154 | 155 | import gensidebar 156 | 157 | gensidebar.generate_sidebar(globals(), "pynetworktables") 158 | -------------------------------------------------------------------------------- /samples/json_logger/logger.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # This is a simple working example of how to use pynetworktables to log data 4 | # to a JSON file. The file is stored in the current working directory, and 5 | # the filename is the current time combined with the event name and match number 6 | # 7 | # The data really can be anything, but this logs a single key. The way it's 8 | # intended to be used is gather your data together, stick it in an array 9 | # along with the current timestamp, then send it all over networktables 10 | # as a number array. 11 | # 12 | # One example way to send the data on the robot is via something like 13 | # wpilib.SmartDashboard.putNumberArray([time, data1, data2, ...]) 14 | # 15 | # The reason this example uses that method is to ensure that all of the data 16 | # received is transmitted at the same time. If you used multiple keys to send 17 | # this data instead, the data could be slightly out of sync with each other. 18 | # 19 | 20 | from networktables import NetworkTables 21 | from networktables.util import ntproperty 22 | 23 | import json 24 | import os.path 25 | import queue 26 | import sys 27 | import time 28 | import threading 29 | 30 | import logging 31 | 32 | logger = logging.getLogger("logger") 33 | 34 | # FMSControlData bitfields 35 | ENABLED_FIELD = 1 << 0 36 | AUTO_FIELD = 1 << 1 37 | TEST_FIELD = 1 << 2 38 | EMERGENCY_STOP_FIELD = 1 << 3 39 | FMS_ATTACHED_FIELD = 1 << 4 40 | DS_ATTACHED_FIELD = 1 << 5 41 | 42 | 43 | def translate_control_word(value): 44 | value = int(value) 45 | if value & ENABLED_FIELD == 0: 46 | return "disabled" 47 | if value & AUTO_FIELD: 48 | return "auto" 49 | if value & TEST_FIELD: 50 | return "test" 51 | else: 52 | return "teleop" 53 | 54 | 55 | class DataLogger: 56 | 57 | # Change this key to whatever NT key you want to log 58 | log_key = "/SmartDashboard/log_data" 59 | 60 | # Data file where robot IP is stored so you don't have to keep typing it 61 | cache_file = ".robot" 62 | 63 | matchNumber = ntproperty("/FMSInfo/MatchNumber", 0, False) 64 | eventName = ntproperty("/FMSInfo/EventName", "unknown", False) 65 | 66 | def __init__(self): 67 | self.queue = queue.Queue() 68 | self.mode = "disabled" 69 | self.data = [] 70 | self.lock = threading.Lock() 71 | 72 | def connectionListener(self, connected, info): 73 | # set our robot to 'disabled' if the connection drops so that we can 74 | # guarantee the data gets written to disk 75 | if not connected: 76 | self.valueChanged("/FMSInfo/FMSControlData", 0, False) 77 | 78 | def valueChanged(self, key, value, isNew): 79 | 80 | if key == "/FMSInfo/FMSControlData": 81 | 82 | mode = translate_control_word(value) 83 | 84 | with self.lock: 85 | last = self.mode 86 | self.mode = mode 87 | 88 | data = self.data 89 | self.data = [] 90 | 91 | logger.info("Robot mode: %s -> %s", last, mode) 92 | 93 | # This example only stores on auto -> disabled transition. Change it 94 | # to whatever it is that you need for logging 95 | if last == "auto": 96 | 97 | tm = time.strftime("%Y%m%d-%H%M-%S") 98 | name = "%s-%s-%s.json" % (tm, self.eventName, int(self.matchNumber)) 99 | logger.info("New file: %s (%d items received)", name, len(data)) 100 | 101 | # We don't write the file from within the NetworkTables callback, 102 | # because we don't want to block the thread. Instead, write it 103 | # to a queue along with the filename so it can be written 104 | # from somewhere else 105 | self.queue.put((name, data)) 106 | 107 | elif key == self.log_key: 108 | if self.mode != "disabled": 109 | with self.lock: 110 | self.data.append(value) 111 | 112 | def run(self): 113 | 114 | # Determine what IP to connect to 115 | try: 116 | server = sys.argv[1] 117 | 118 | # Save the robot ip 119 | if not os.path.exists(self.cache_file): 120 | with open(self.cache_file, "w") as fp: 121 | fp.write(server) 122 | except IndexError: 123 | try: 124 | with open(self.cache_file) as fp: 125 | server = fp.read().strip() 126 | except IOError: 127 | print("Usage: logger.py [10.xx.yy.2]") 128 | return 129 | 130 | logger.info("NT server set to %s", server) 131 | NetworkTables.initialize(server=server) 132 | 133 | # Use listeners to receive the data 134 | NetworkTables.addConnectionListener( 135 | self.connectionListener, immediateNotify=True 136 | ) 137 | NetworkTables.addEntryListener(self.valueChanged) 138 | 139 | # When new data is queued, write it to disk 140 | while True: 141 | name, data = self.queue.get() 142 | with open(name, "w") as fp: 143 | json.dump(data, fp) 144 | 145 | 146 | if __name__ == "__main__": 147 | 148 | log_datefmt = "%H:%M:%S" 149 | log_format = "%(asctime)s:%(msecs)03d %(levelname)-8s: %(name)-20s: %(message)s" 150 | 151 | logging.basicConfig(level=logging.DEBUG, datefmt=log_datefmt, format=log_format) 152 | 153 | dl = DataLogger() 154 | dl.run() 155 | -------------------------------------------------------------------------------- /tests/test_network_table.py: -------------------------------------------------------------------------------- 1 | # 2 | # These tests are leftover from the original pynetworktables tests 3 | # 4 | 5 | import pytest 6 | 7 | 8 | @pytest.fixture(scope="function") 9 | def table1(nt): 10 | return nt.getTable("/test1") 11 | 12 | 13 | @pytest.fixture(scope="function") 14 | def table2(nt): 15 | return nt.getTable("/test2") 16 | 17 | 18 | def test_put_double(table1): 19 | 20 | table1.putNumber("double", 42.42) 21 | assert table1.getNumber("double", None) == 42.42 22 | 23 | assert table1.getNumber("Non-Existant", 44.44) == 44.44 24 | 25 | 26 | def test_put_boolean(table1): 27 | 28 | table1.putBoolean("boolean", True) 29 | assert table1.getBoolean("boolean", None) == True 30 | 31 | assert table1.getBoolean("Non-Existant", False) == False 32 | 33 | 34 | def test_put_string(table1): 35 | 36 | table1.putString("String", "Test 1") 37 | assert table1.getString("String", None) == "Test 1" 38 | 39 | assert table1.getString("Non-Existant", "Test 3") == "Test 3" 40 | 41 | 42 | def test_multi_data_type(table1): 43 | 44 | table1.putNumber("double1", 1) 45 | table1.putNumber("double2", 2) 46 | table1.putNumber("double3", 3) 47 | table1.putBoolean("bool1", False) 48 | table1.putBoolean("bool2", True) 49 | table1.putString("string1", "String 1") 50 | table1.putString("string2", "String 2") 51 | table1.putString("string3", "String 3") 52 | 53 | assert table1.getNumber("double1", None) == 1 54 | assert table1.getNumber("double2", None) == 2 55 | assert table1.getNumber("double3", None) == 3 56 | assert table1.getBoolean("bool1", None) == False 57 | assert table1.getBoolean("bool2", None) == True 58 | assert table1.getString("string1", None) == "String 1" 59 | assert table1.getString("string2", None) == "String 2" 60 | assert table1.getString("string3", None) == "String 3" 61 | 62 | table1.putNumber("double1", 4) 63 | table1.putNumber("double2", 5) 64 | table1.putNumber("double3", 6) 65 | table1.putBoolean("bool1", True) 66 | table1.putBoolean("bool2", False) 67 | table1.putString("string1", "String 4") 68 | table1.putString("string2", "String 5") 69 | table1.putString("string3", "String 6") 70 | 71 | assert table1.getNumber("double1", None) == 4 72 | assert table1.getNumber("double2", None) == 5 73 | assert table1.getNumber("double3", None) == 6 74 | assert table1.getBoolean("bool1", None) == True 75 | assert table1.getBoolean("bool2", None) == False 76 | assert table1.getString("string1", None) == "String 4" 77 | assert table1.getString("string2", None) == "String 5" 78 | assert table1.getString("string3", None) == "String 6" 79 | 80 | 81 | def test_multi_table(table1, table2): 82 | 83 | table1.putNumber("table1double", 1) 84 | table1.putBoolean("table1boolean", True) 85 | table1.putString("table1string", "Table 1") 86 | 87 | assert table2.getNumber("table1double", None) == None 88 | assert table2.getBoolean("table1boolean", None) == None 89 | assert table2.getString("table1string", None) == None 90 | 91 | table2.putNumber("table2double", 2) 92 | table2.putBoolean("table2boolean", False) 93 | table2.putString("table2string", "Table 2") 94 | 95 | assert table1.getNumber("table2double", None) == None 96 | assert table1.getBoolean("table2boolean", None) == None 97 | assert table1.getString("table2string", None) == None 98 | 99 | 100 | def test_get_table(nt, table1, table2): 101 | assert nt.getTable("test1") is table1 102 | assert nt.getTable("test2") is table2 103 | 104 | assert nt.getTable("/test1") is table1 105 | assert nt.getTable("/test2") is table2 106 | 107 | assert nt.getTable("/test1/") is table1 108 | assert nt.getTable("/test1/").path == "/test1" 109 | 110 | assert table1 is not table2 111 | 112 | table3 = nt.getTable("/test3") 113 | assert table1 is not table3 114 | assert table2 is not table3 115 | 116 | 117 | def test_get_subtable(nt, table1): 118 | assert not table1.containsSubTable("test1") 119 | 120 | st1 = table1.getSubTable("test1") 121 | 122 | assert nt.getTable("/test1/test1") is st1 123 | assert table1.getSubTable("test1") is st1 124 | 125 | # weird, but true -- subtable only exists when key exists 126 | assert not table1.containsSubTable("test1") 127 | st1.putBoolean("hi", True) 128 | assert table1.containsSubTable("test1") 129 | 130 | assert table1.getSubTables() == ["test1"] 131 | assert st1.getSubTables() == [] 132 | 133 | 134 | def test_getkeys(table1): 135 | assert table1.getKeys() == [] 136 | assert not table1.containsKey("hi") 137 | assert "hi" not in table1 138 | 139 | table1.putBoolean("hi", True) 140 | assert table1.getKeys() == ["hi"] 141 | 142 | assert table1.containsKey("hi") 143 | assert "hi" in table1 144 | 145 | 146 | def test_flags(table1): 147 | table1.putBoolean("foo", True) 148 | assert not table1.isPersistent("foo") 149 | 150 | table1.setPersistent("foo") 151 | assert table1.isPersistent("foo") 152 | 153 | table1.clearPersistent("foo") 154 | assert not table1.isPersistent("foo") 155 | 156 | 157 | def test_delete(table1): 158 | table1.putBoolean("foo", True) 159 | assert table1.getBoolean("foo", None) == True 160 | 161 | table1.delete("foo") 162 | assert table1.getBoolean("foo", None) == None 163 | 164 | 165 | def test_different_type(table1): 166 | assert table1.putBoolean("foo", True) 167 | assert table1.getBoolean("foo", None) == True 168 | 169 | assert not table1.putNumber("foo", 1) 170 | assert table1.getBoolean("foo", None) == True 171 | -------------------------------------------------------------------------------- /_pynetworktables/_impl/storage_load.py: -------------------------------------------------------------------------------- 1 | # validated: 2019-02-26 DS 0e1f9c2ed271 cpp/Storage_load.cpp 2 | 3 | import ast 4 | import binascii 5 | import base64 6 | import re 7 | from configparser import RawConfigParser, NoSectionError 8 | 9 | from .value import Value 10 | 11 | import logging 12 | 13 | logger = logging.getLogger("nt") 14 | 15 | 16 | PERSISTENT_SECTION = "NetworkTables Storage 3.0" 17 | 18 | _key_bool = re.compile('boolean "(.+)"') 19 | _key_double = re.compile('double "(.+)"') 20 | _key_string = re.compile('string "(.+)"') 21 | _key_raw = re.compile('raw "(.+)"') 22 | _key_bool_array = re.compile('array boolean "(.+)"') 23 | _key_double_array = re.compile('array double "(.+)"') 24 | _key_string_array = re.compile('array string "(.+)"') 25 | 26 | _value_string = re.compile(r'"((?:\\.|[^"\\])*)",?') 27 | 28 | # TODO: these escape functions almost certainly don't deal with unicode 29 | # correctly 30 | 31 | # TODO: strictly speaking, this isn't 100% compatible with ntcore... but 32 | 33 | 34 | def _unescape_string(s): 35 | # shortcut if no escapes present 36 | if "\\" not in s: 37 | return s 38 | 39 | # let python do the hard work 40 | return ast.literal_eval('"%s"' % s) 41 | 42 | 43 | def load_entries(fp, filename, prefix): 44 | 45 | entries = [] 46 | 47 | parser = RawConfigParser() 48 | parser.optionxform = str 49 | 50 | try: 51 | if hasattr(parser, "read_file"): 52 | parser.read_file(fp, filename) 53 | else: 54 | parser.readfp(fp, filename) 55 | except IOError: 56 | raise 57 | except Exception as e: 58 | raise IOError("Error reading persistent file: %s" % e) 59 | 60 | try: 61 | items = parser.items(PERSISTENT_SECTION) 62 | except NoSectionError: 63 | raise IOError("Persistent section not found") 64 | 65 | value = None 66 | m = None 67 | 68 | for k, v in items: 69 | 70 | # Reduces code duplication 71 | if value: 72 | key = _unescape_string(m.group(1)) 73 | if key.startswith(prefix): 74 | entries.append((key, value)) 75 | 76 | value = None 77 | 78 | m = _key_bool.match(k) 79 | if m: 80 | if v == "true": 81 | value = Value.makeBoolean(True) 82 | elif v == "false": 83 | value = Value.makeBoolean(False) 84 | else: 85 | logger.warning("Unrecognized boolean value %r for %s", v, m.group(1)) 86 | continue 87 | 88 | m = _key_double.match(k) 89 | if m: 90 | try: 91 | value = Value.makeDouble(float(v)) 92 | except ValueError as e: 93 | logger.warning("Unrecognized double value %r for %s", v, m.group(1)) 94 | 95 | continue 96 | 97 | m = _key_string.match(k) 98 | if m: 99 | mm = _value_string.match(v) 100 | 101 | if mm: 102 | value = Value.makeString(_unescape_string(mm.group(1))) 103 | else: 104 | logger.warning("Unrecognized string value %r for %s", v, m.group(1)) 105 | continue 106 | 107 | m = _key_raw.match(k) 108 | if m: 109 | try: 110 | v = base64.b64decode(v, validate=True) 111 | value = Value.makeRaw(v) 112 | except binascii.Error: 113 | logger.warning("Unrecognized raw value %r for %s", v, m.group(1)) 114 | continue 115 | 116 | m = _key_bool_array.match(k) 117 | if m: 118 | bools = [] 119 | arr = v.strip().split(",") 120 | if arr != [""]: 121 | for vv in arr: 122 | vv = vv.strip() 123 | if vv == "true": 124 | bools.append(True) 125 | elif vv == "false": 126 | bools.append(False) 127 | else: 128 | logger.warning( 129 | "Unrecognized bool '%s' in bool array %s'", vv, m.group(1) 130 | ) 131 | bools = None 132 | break 133 | 134 | if bools is not None: 135 | value = Value.makeBooleanArray(bools) 136 | continue 137 | 138 | m = _key_double_array.match(k) 139 | if m: 140 | doubles = [] 141 | arr = v.strip().split(",") 142 | if arr != [""]: 143 | for vv in arr: 144 | try: 145 | doubles.append(float(vv)) 146 | except ValueError: 147 | logger.warning( 148 | "Unrecognized double '%s' in double array %s", 149 | vv, 150 | m.group(1), 151 | ) 152 | doubles = None 153 | break 154 | 155 | value = Value.makeDoubleArray(doubles) 156 | continue 157 | 158 | m = _key_string_array.match(k) 159 | if m: 160 | # Technically, this will let invalid inputs in... but, 161 | # I don't really care. Feel free to fix it if you do. 162 | strings = [_unescape_string(vv) for vv in _value_string.findall(v)] 163 | value = Value.makeStringArray(strings) 164 | continue 165 | 166 | logger.warning("Unrecognized type '%s'", k) 167 | 168 | if value: 169 | key = _unescape_string(m.group(1)) 170 | if key.startswith(prefix): 171 | entries.append((key, value)) 172 | 173 | return entries 174 | -------------------------------------------------------------------------------- /tests/test_api.py: -------------------------------------------------------------------------------- 1 | # 2 | # These tests stand up a separate client and server instance of 3 | # networktables and tests the 'real' user API to ensure that it 4 | # works correctly 5 | # 6 | 7 | from __future__ import print_function 8 | 9 | import pytest 10 | 11 | import logging 12 | 13 | logger = logging.getLogger("test") 14 | 15 | 16 | # test defaults 17 | def doc(nt): 18 | t = nt.getTable("nope") 19 | 20 | assert t.getBoolean("b", None) is None 21 | assert t.getNumber("n", None) is None 22 | assert t.getString("s", None) is None 23 | assert t.getBooleanArray("ba", None) is None 24 | assert t.getNumberArray("na", None) is None 25 | assert t.getStringArray("sa", None) is None 26 | assert t.getValue("v", None) is None 27 | 28 | assert t.getBoolean("b", True) is True 29 | assert t.getNumber("n", 1) == 1 30 | assert t.getString("s", "sss") == "sss" 31 | assert t.getBooleanArray("ba", (True,)) == (True,) 32 | assert t.getNumberArray("na", (1,)) == (1,) 33 | assert t.getStringArray("sa", ("ss",)) == ("ss",) 34 | assert t.getValue("v", "vvv") == "vvv" 35 | 36 | 37 | def do(nt1, nt2, t): 38 | 39 | t1 = nt1.getTable(t) 40 | with nt2.expect_changes(8): 41 | t1.putBoolean("bool", True) 42 | t1.putNumber("number1", 1) 43 | t1.putNumber("number2", 1.5) 44 | t1.putString("string", "string") 45 | t1.putString("unicode", u"\xA9") # copyright symbol 46 | t1.putBooleanArray("ba", (True, False)) 47 | t1.putNumberArray("na", (1, 2)) 48 | t1.putStringArray("sa", ("s", "t")) 49 | 50 | t2 = nt2.getTable(t) 51 | assert t2.getBoolean("bool", None) is True 52 | assert t2.getNumber("number1", None) == 1 53 | assert t2.getNumber("number2", None) == 1.5 54 | assert t2.getString("string", None) == "string" 55 | assert t2.getString("unicode", None) == u"\xA9" # copyright symbol 56 | assert t2.getBooleanArray("ba", None) == (True, False) 57 | assert t2.getNumberArray("na", None) == (1, 2) 58 | assert t2.getStringArray("sa", None) == ("s", "t") 59 | 60 | # Value testing 61 | with nt2.expect_changes(6): 62 | t1.putValue("v_b", False) 63 | t1.putValue("v_n1", 2) 64 | t1.putValue("v_n2", 2.5) 65 | t1.putValue("v_s", "ssss") 66 | t1.putValue("v_s2", u"\xA9") 67 | 68 | t1.putValue("v_v", 0) 69 | 70 | print(t2.getKeys()) 71 | assert t2.getBoolean("v_b", None) is False 72 | assert t2.getNumber("v_n1", None) == 2 73 | assert t2.getNumber("v_n2", None) == 2.5 74 | assert t2.getString("v_s", None) == "ssss" 75 | assert t2.getString("v_s2", None) == u"\xA9" 76 | assert t2.getValue("v_v", None) == 0 77 | 78 | # Ensure that updating values work! 79 | with nt2.expect_changes(8): 80 | t1.putBoolean("bool", False) 81 | t1.putNumber("number1", 2) 82 | t1.putNumber("number2", 2.5) 83 | t1.putString("string", "sss") 84 | t1.putString("unicode", u"\u2122") # (tm) 85 | t1.putBooleanArray("ba", (False, True, False)) 86 | t1.putNumberArray("na", (2, 1)) 87 | t1.putStringArray("sa", ("t", "s")) 88 | 89 | t2 = nt2.getTable(t) 90 | assert t2.getBoolean("bool", None) is False 91 | assert t2.getNumber("number1", None) == 2 92 | assert t2.getNumber("number2", None) == 2.5 93 | assert t2.getString("string", None) == "sss" 94 | assert t2.getString("unicode", None) == u"\u2122" 95 | assert t2.getBooleanArray("ba", None) == (False, True, False) 96 | assert t2.getNumberArray("na", None) == (2, 1) 97 | assert t2.getStringArray("sa", None) == ("t", "s") 98 | 99 | # Try out deletes -- but NT2 doesn't support them 100 | if nt2.proto_rev == 0x0300: 101 | if nt1.proto_rev == 0x0300: 102 | with nt2.expect_changes(1): 103 | t1.delete("bool") 104 | 105 | assert t2.getBoolean("bool", None) == None 106 | else: 107 | t1.delete("bool") 108 | 109 | with nt2.expect_changes(1): 110 | t1.putBoolean("ooo", True) 111 | 112 | assert t2.getBoolean("bool", None) is False 113 | 114 | else: 115 | t1.delete("bool") 116 | 117 | with nt2.expect_changes(1): 118 | t1.putBoolean("ooo", True) 119 | 120 | assert t2.getBoolean("bool", None) is False 121 | 122 | 123 | def test_basic(nt_live): 124 | 125 | nt_server, nt_client = nt_live 126 | 127 | assert nt_server.isServer() 128 | assert not nt_client.isServer() 129 | 130 | doc(nt_client) 131 | doc(nt_server) 132 | 133 | # server -> client 134 | do(nt_server, nt_client, "server2client") 135 | 136 | # client -> server 137 | do(nt_client, nt_server, "client2server") 138 | 139 | assert nt_client.isConnected() 140 | assert nt_server.isConnected() 141 | 142 | 143 | def test_reconnect(nt_live): 144 | 145 | nt_server, nt_client = nt_live 146 | 147 | with nt_server.expect_changes(1): 148 | ct = nt_client.getTable("t") 149 | ct.putBoolean("foo", True) 150 | 151 | st = nt_server.getTable("t") 152 | assert st.getBoolean("foo", None) == True 153 | 154 | # Client disconnect testing 155 | nt_client.shutdown() 156 | 157 | logger.info("Shutdown the client") 158 | 159 | with nt_client.expect_changes(1): 160 | nt_client.start_test() 161 | ct = nt_client.getTable("t") 162 | 163 | assert ct.getBoolean("foo", None) == True 164 | 165 | # Server disconnect testing 166 | nt_server.shutdown() 167 | logger.info("Shutdown the server") 168 | 169 | # synchronization change: if the client doesn't touch the entry locally, 170 | # then it won't get transferred back to the server on reconnect. Touch 171 | # it here to ensure that it comes back 172 | ct.putBoolean("foo", True) 173 | 174 | with nt_server.expect_changes(1): 175 | nt_server.start_test() 176 | 177 | st = nt_server.getTable("t") 178 | assert st.getBoolean("foo", None) == True 179 | -------------------------------------------------------------------------------- /networktables/util.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, Optional, Sequence 2 | 3 | from . import NetworkTablesInstance 4 | 5 | __all__ = ["ntproperty", "ChooserControl"] 6 | 7 | NetworkTables = NetworkTablesInstance.getDefault() 8 | 9 | 10 | class _NtProperty: 11 | def __init__( 12 | self, 13 | key: str, 14 | defaultValue, 15 | writeDefault: bool, 16 | persistent: bool, 17 | inst: NetworkTablesInstance, 18 | ) -> None: 19 | self.key = key 20 | self.defaultValue = defaultValue 21 | self.writeDefault = writeDefault 22 | self.persistent = persistent 23 | # never overwrite persistent values with defaults 24 | if persistent: 25 | self.writeDefault = False 26 | self.inst = inst 27 | if hasattr(self.inst, "_api"): 28 | self.set = self._set_pynetworktables 29 | else: 30 | self.set = self._set_pyntcore 31 | 32 | self.reset() 33 | 34 | def reset(self): 35 | self.ntvalue = self.inst.getGlobalAutoUpdateValue( 36 | self.key, self.defaultValue, self.writeDefault 37 | ) 38 | if self.persistent: 39 | self.ntvalue.setPersistent() 40 | 41 | if hasattr(self.inst, "_api"): 42 | from _pynetworktables import Value 43 | else: 44 | from . import Value 45 | 46 | # this is an optimization, but presumes the value type never changes 47 | self.mkv = Value.getFactoryByType(self.ntvalue.getType()) 48 | 49 | def get(self, _): 50 | return self.ntvalue.value 51 | 52 | def _set_pynetworktables(self, _, value): 53 | self.inst._api.setEntryValueById(self.ntvalue._local_id, self.mkv(value)) 54 | 55 | def _set_pyntcore(self, _, value): 56 | self.ntvalue.setValue(self.mkv(value)) 57 | 58 | 59 | def ntproperty( 60 | key: str, 61 | defaultValue, 62 | writeDefault: bool = True, 63 | doc: str = None, 64 | persistent: bool = False, 65 | *, 66 | inst: NetworkTablesInstance = NetworkTables 67 | ) -> property: 68 | """ 69 | A property that you can add to your classes to access NetworkTables 70 | variables like a normal variable. 71 | 72 | :param key: A full NetworkTables key (eg ``/SmartDashboard/foo``) 73 | :param defaultValue: Default value to use if not in the table 74 | :type defaultValue: any 75 | :param writeDefault: If True, put the default value to the table, 76 | overwriting existing values 77 | :param doc: If given, will be the docstring of the property. 78 | :param persistent: If True, persist set values across restarts. 79 | *writeDefault* is ignored if this is True. 80 | :param inst: The NetworkTables instance to use. 81 | 82 | Example usage:: 83 | 84 | class Foo(object): 85 | 86 | something = ntproperty('/SmartDashboard/something', True) 87 | 88 | ... 89 | 90 | def do_thing(self): 91 | if self.something: # reads from value 92 | ... 93 | 94 | self.something = False # writes value 95 | 96 | .. note:: Does not work with empty lists/tuples. 97 | 98 | Getting the value of this property should be reasonably 99 | fast, but setting the value will have just as much overhead 100 | as :meth:`.NetworkTable.putValue` 101 | 102 | .. warning:: 103 | 104 | This function assumes that the value's type 105 | never changes. If it does, you'll get really strange 106 | errors... so don't do that. 107 | 108 | .. versionadded:: 2015.3.0 109 | 110 | .. versionchanged:: 2017.0.6 111 | The *doc* parameter. 112 | 113 | .. versionchanged:: 2018.0.0 114 | The *persistent* parameter. 115 | """ 116 | ntprop = _NtProperty(key, defaultValue, writeDefault, persistent, inst) 117 | try: 118 | inst._ntproperties.add(ntprop) 119 | except AttributeError: 120 | pass # pyntcore compat 121 | 122 | return property(fget=ntprop.get, fset=ntprop.set, doc=doc) 123 | 124 | 125 | class ChooserControl(object): 126 | """ 127 | Interacts with a :class:`wpilib.SendableChooser` 128 | object over NetworkTables. 129 | """ 130 | 131 | def __init__( 132 | self, 133 | key: str, 134 | on_choices: Optional[Callable[[Sequence[str]], None]] = None, 135 | on_selected: Optional[Callable[[str], None]] = None, 136 | *, 137 | inst: NetworkTablesInstance = NetworkTables 138 | ) -> None: 139 | """ 140 | :param key: NetworkTables key 141 | :param on_choices: A function that will be called when the 142 | choices change. 143 | :param on_selection: A function that will be called when the 144 | selection changes. 145 | :param inst: The NetworkTables instance to use. 146 | """ 147 | self.subtable = inst.getTable("SmartDashboard").getSubTable(key) 148 | 149 | self.on_choices = on_choices 150 | self.on_selected = on_selected 151 | 152 | if on_choices or on_selected: 153 | self.subtable.addTableListener(self._on_change, True) 154 | 155 | def close(self) -> None: 156 | """Stops listening for changes to the ``SendableChooser``""" 157 | if self.on_choices or self.on_selected: 158 | self.subtable.removeTableListener(self._on_change) 159 | 160 | def getChoices(self) -> Sequence[str]: 161 | """ 162 | Returns the current choices. If the chooser doesn't exist, this 163 | will return an empty tuple. 164 | """ 165 | return self.subtable.getStringArray("options", ()) 166 | 167 | def getSelected(self) -> Optional[str]: 168 | """ 169 | Returns the current selection or None 170 | """ 171 | selected = self.subtable.getString("selected", None) 172 | if selected is None: 173 | selected = self.subtable.getString("default", None) 174 | return selected 175 | 176 | def setSelected(self, selection: str) -> None: 177 | """ 178 | Sets the active selection on the chooser 179 | 180 | :param selection: Active selection name 181 | """ 182 | self.subtable.putString("selected", selection) 183 | 184 | def _on_change(self, table, key, value, isNew): 185 | if key == "options": 186 | if self.on_choices is not None: 187 | self.on_choices(value) 188 | elif key == "selected": 189 | if self.on_selected is not None: 190 | self.on_selected(value) 191 | elif key == "default": 192 | if ( 193 | self.on_selected is not None 194 | and self.subtable.getString("selected", None) is None 195 | ): 196 | self.on_selected(value) 197 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # 2 | # Useful fixtures 3 | # 4 | 5 | from contextlib import contextmanager 6 | from threading import Condition 7 | 8 | log_datefmt = "%H:%M:%S" 9 | log_format = "%(asctime)s:%(msecs)03d %(levelname)-8s: %(name)-8s: %(message)s" 10 | 11 | import logging 12 | 13 | logging.basicConfig(level=logging.DEBUG, format=log_format, datefmt=log_datefmt) 14 | 15 | 16 | logger = logging.getLogger("conftest") 17 | 18 | import pytest 19 | 20 | from _pynetworktables import NetworkTables, NetworkTablesInstance 21 | 22 | # 23 | # Fixtures for a usable in-memory version of networktables 24 | # 25 | 26 | 27 | @pytest.fixture(scope="function", params=[True, False]) 28 | def verbose_logging(request): 29 | return request.param 30 | 31 | 32 | @pytest.fixture(scope="function", params=[True, False]) 33 | def nt(request): 34 | """Starts/stops global networktables instance for testing""" 35 | NetworkTables.startTestMode(server=request.param) 36 | 37 | yield NetworkTables 38 | 39 | NetworkTables.shutdown() 40 | 41 | 42 | @pytest.fixture(scope="function") 43 | def entry_notifier(nt): 44 | return nt._api.entry_notifier 45 | 46 | 47 | @pytest.fixture(scope="function") 48 | def conn_notifier(nt): 49 | return nt._api.conn_notifier 50 | 51 | 52 | @pytest.fixture(scope="function") 53 | def nt_flush(nt): 54 | """Flushes NT key notifications""" 55 | 56 | def _flush(): 57 | assert nt._api.waitForEntryListenerQueue(1.0) 58 | assert nt._api.waitForConnectionListenerQueue(1.0) 59 | 60 | return _flush 61 | 62 | 63 | # 64 | # Live NT instance fixtures 65 | # 66 | 67 | 68 | class NtTestBase(NetworkTablesInstance): 69 | """ 70 | Object for managing a live pair of NT server/client 71 | """ 72 | 73 | _wait_lock = None 74 | _testing_verbose_logging = True 75 | 76 | def shutdown(self): 77 | logger.info("shutting down %s", self.__class__.__name__) 78 | NetworkTablesInstance.shutdown(self) 79 | if self._wait_lock is not None: 80 | self._wait_init_listener() 81 | 82 | def disconnect(self): 83 | self._api.dispatcher.stop() 84 | 85 | def _init_common(self, proto_rev): 86 | # This resets the instance to be independent 87 | self.shutdown() 88 | self._api.dispatcher.setDefaultProtoRev(proto_rev) 89 | self.proto_rev = proto_rev 90 | 91 | if self._testing_verbose_logging: 92 | self.enableVerboseLogging() 93 | # self._wait_init() 94 | 95 | def _init_server(self, proto_rev, server_port=0): 96 | self._init_common(proto_rev) 97 | 98 | self.port = server_port 99 | 100 | def _init_client(self, proto_rev): 101 | self._init_common(proto_rev) 102 | 103 | def _wait_init(self): 104 | self._wait_lock = Condition() 105 | self._wait = 0 106 | self._wait_init_listener() 107 | 108 | def _wait_init_listener(self): 109 | self._api.addEntryListener( 110 | "", 111 | self._wait_cb, 112 | NetworkTablesInstance.NotifyFlags.NEW 113 | | NetworkTablesInstance.NotifyFlags.UPDATE 114 | | NetworkTablesInstance.NotifyFlags.DELETE 115 | | NetworkTablesInstance.NotifyFlags.FLAGS, 116 | ) 117 | 118 | def _wait_cb(self, *args): 119 | with self._wait_lock: 120 | self._wait += 1 121 | # logger.info('Wait callback, got: %s', args) 122 | self._wait_lock.notify() 123 | 124 | @contextmanager 125 | def expect_changes(self, count): 126 | """Use this on the *other* instance that you're making 127 | changes on, to wait for the changes to propagate to the 128 | other instance""" 129 | 130 | if self._wait_lock is None: 131 | self._wait_init() 132 | 133 | with self._wait_lock: 134 | self._wait = 0 135 | 136 | logger.info("Begin actions") 137 | yield 138 | logger.info("Waiting for %s changes", count) 139 | 140 | with self._wait_lock: 141 | result, msg = ( 142 | self._wait_lock.wait_for(lambda: self._wait == count, 4), 143 | "Timeout waiting for %s changes (got %s)" % (count, self._wait), 144 | ) 145 | logger.info("expect_changes: %s %s", result, msg) 146 | assert result, msg 147 | 148 | 149 | # Each test should cover each NT version combination 150 | # 0x0200 -> 0x0300 151 | # 0x0300 -> 0x0200 152 | # 0x0300 -> 0x0300 153 | 154 | 155 | @pytest.fixture(params=[0x0200, 0x0300]) 156 | def nt_server(request, verbose_logging): 157 | class NtServer(NtTestBase): 158 | 159 | _test_saved_port = None 160 | _testing_verbose_logging = verbose_logging 161 | 162 | def start_test(self): 163 | logger.info("NtServer::start_test") 164 | 165 | # Restore server port on restart 166 | if self._test_saved_port is not None: 167 | self.port = self._test_saved_port 168 | self._api.dispatcher.setDefaultProtoRev(request.param) 169 | 170 | if verbose_logging: 171 | self.enableVerboseLogging() 172 | 173 | self.startServer(listenAddress="127.0.0.1", port=self.port) 174 | 175 | assert self._api.dispatcher.m_server_acceptor.waitForStart(timeout=1) 176 | self.port = self._api.dispatcher.m_server_acceptor.m_port 177 | self._test_saved_port = self.port 178 | 179 | server = NtServer() 180 | server._init_server(request.param) 181 | yield server 182 | server.shutdown() 183 | 184 | 185 | @pytest.fixture(params=[0x0200, 0x0300]) 186 | def nt_client(request, nt_server, verbose_logging): 187 | class NtClient(NtTestBase): 188 | 189 | _testing_verbose_logging = verbose_logging 190 | 191 | def start_test(self): 192 | if verbose_logging: 193 | self.enableVerboseLogging() 194 | self.setNetworkIdentity("C1") 195 | self._api.dispatcher.setDefaultProtoRev(request.param) 196 | self.startClient(("127.0.0.1", nt_server.port)) 197 | 198 | client = NtClient() 199 | client._init_client(request.param) 200 | yield client 201 | client.shutdown() 202 | 203 | 204 | @pytest.fixture(params=[0x0300]) # don't bother with other proto versions 205 | def nt_client2(request, nt_server, verbose_logging): 206 | class NtClient(NtTestBase): 207 | 208 | _testing_verbose_logging = verbose_logging 209 | 210 | def start_test(self): 211 | if verbose_logging: 212 | self.enableVerboseLogging() 213 | self._api.dispatcher.setDefaultProtoRev(request.param) 214 | self.setNetworkIdentity("C2") 215 | self.startClient(("127.0.0.1", nt_server.port)) 216 | 217 | client = NtClient() 218 | client._init_client(request.param) 219 | yield client 220 | client.shutdown() 221 | 222 | 223 | @pytest.fixture 224 | def nt_live(nt_server, nt_client): 225 | """This fixture automatically starts the client and server""" 226 | 227 | nt_server.start_test() 228 | nt_client.start_test() 229 | 230 | return nt_server, nt_client 231 | -------------------------------------------------------------------------------- /tests/test_wire.py: -------------------------------------------------------------------------------- 1 | # 2 | # These tests are adapted from ntcore's test suite 3 | # 4 | 5 | from __future__ import print_function 6 | 7 | from io import BytesIO 8 | 9 | import pytest 10 | 11 | from _pynetworktables._impl.message import Message 12 | from _pynetworktables._impl.value import Value 13 | from _pynetworktables._impl.tcpsockets.tcp_stream import TCPStream, StreamEOF 14 | from _pynetworktables._impl.wire import WireCodec 15 | 16 | 17 | class ReadStream(TCPStream): 18 | def __init__(self, fp): 19 | self.m_rdsock = fp 20 | 21 | 22 | @pytest.fixture(params=[0x0200, 0x0300]) 23 | def proto_rev(request): 24 | return request.param 25 | 26 | 27 | @pytest.fixture 28 | def v_round_trip(proto_rev): 29 | 30 | codec = WireCodec(proto_rev) 31 | 32 | def _fn(v, minver=0x0200): 33 | if codec.proto_rev < minver: 34 | return 35 | 36 | out = [] 37 | fp = BytesIO() 38 | rstream = ReadStream(fp) 39 | 40 | codec.write_value(v, out) 41 | fp.write(b"".join(out)) 42 | fp.seek(0) 43 | 44 | vv = codec.read_value(v.type, rstream) 45 | 46 | with pytest.raises(StreamEOF): 47 | rstream.read(1) 48 | 49 | assert v == vv 50 | 51 | return _fn 52 | 53 | 54 | # for each value type, test roundtrip 55 | 56 | 57 | def test_wire_boolean(v_round_trip): 58 | v_round_trip(Value.makeBoolean(True)) 59 | 60 | 61 | def test_wire_double(v_round_trip): 62 | v_round_trip(Value.makeDouble(0.5)) 63 | 64 | 65 | def test_wire_string1(v_round_trip): 66 | v_round_trip(Value.makeString("")) 67 | 68 | 69 | def test_wire_string2(v_round_trip): 70 | v_round_trip(Value.makeString("Hi there")) 71 | 72 | 73 | def test_wire_raw1(v_round_trip): 74 | v_round_trip(Value.makeRaw(b""), minver=0x0300) 75 | 76 | 77 | def test_wire_raw2(v_round_trip): 78 | v_round_trip(Value.makeRaw(b"\x00\xff\x78"), minver=0x0300) 79 | 80 | 81 | def test_wire_boolArray1(v_round_trip): 82 | v_round_trip(Value.makeBooleanArray([])) 83 | 84 | 85 | def test_wire_boolArray2(v_round_trip): 86 | v_round_trip(Value.makeBooleanArray([True, False])) 87 | 88 | 89 | def test_wire_boolArray3(v_round_trip): 90 | v_round_trip(Value.makeBooleanArray([True] * 255)) 91 | 92 | 93 | def test_wire_doubleArray1(v_round_trip): 94 | v_round_trip(Value.makeDoubleArray([])) 95 | 96 | 97 | def test_wire_doubleArray2(v_round_trip): 98 | v_round_trip(Value.makeDoubleArray([0, 1])) 99 | 100 | 101 | def test_wire_doubleArray3(v_round_trip): 102 | v_round_trip(Value.makeDoubleArray([0] * 255)) 103 | 104 | 105 | def test_wire_stringArray1(v_round_trip): 106 | v_round_trip(Value.makeStringArray([])) 107 | 108 | 109 | def test_wire_stringArray2(v_round_trip): 110 | v_round_trip(Value.makeStringArray(["hi", "there"])) 111 | 112 | 113 | def test_wire_stringArray3(v_round_trip): 114 | v_round_trip(Value.makeStringArray(["hi"] * 255)) 115 | 116 | 117 | def test_wire_rpc1(v_round_trip): 118 | v_round_trip(Value.makeRpc(""), minver=0x0300) 119 | 120 | 121 | def test_wire_rpc2(v_round_trip): 122 | v_round_trip(Value.makeRpc("Hi there"), minver=0x0300) 123 | 124 | 125 | # Try out the various message types 126 | @pytest.fixture 127 | def msg_round_trip(proto_rev): 128 | 129 | codec = WireCodec(proto_rev) 130 | 131 | def _fn(msg: Message, minver=0x0200, exclude=None): 132 | 133 | out = [] 134 | fp = BytesIO() 135 | rstream = ReadStream(fp) 136 | 137 | if codec.proto_rev < minver: 138 | # The codec won't have the correct struct set if 139 | # the version isn't supported 140 | msg.write(out, codec) 141 | assert not out 142 | return 143 | 144 | msg.write(out, codec) 145 | fp.write(b"".join(out)) 146 | fp.seek(0) 147 | 148 | mm = Message.read(rstream, codec, lambda x: msg.value.type) 149 | 150 | with pytest.raises(StreamEOF): 151 | rstream.read(1) 152 | 153 | # In v2, some fields aren't copied over, so we exclude them 154 | # by overwriting those indices and recreating the read message 155 | if exclude: 156 | args = list(mm) 157 | for e in exclude: 158 | args[e] = msg[e] 159 | mm = Message(*args) 160 | 161 | assert msg == mm 162 | 163 | return _fn 164 | 165 | 166 | def test_wire_keepAlive(msg_round_trip): 167 | msg_round_trip(Message.keepAlive()) 168 | 169 | 170 | def test_wire_clientHello(msg_round_trip): 171 | msg_round_trip(Message.clientHello(0x0200, "Hi"), exclude=[1]) 172 | 173 | 174 | def test_wire_clientHelloV3(msg_round_trip): 175 | msg_round_trip(Message.clientHello(0x0300, "Hi")) 176 | 177 | 178 | def test_wire_protoUnsup(msg_round_trip): 179 | msg_round_trip(Message.protoUnsup(0x0300)) 180 | 181 | 182 | def test_wire_serverHelloDone(msg_round_trip): 183 | msg_round_trip(Message.serverHelloDone()) 184 | 185 | 186 | def test_wire_serverHello(msg_round_trip): 187 | msg_round_trip(Message.serverHello(0x01, "Hi"), minver=0x0300) 188 | 189 | 190 | def test_wire_clientHelloDone(msg_round_trip): 191 | msg_round_trip(Message.clientHelloDone()) 192 | 193 | 194 | def test_wire_entryAssign1(msg_round_trip, proto_rev): 195 | exclude = [] if proto_rev >= 0x0300 else [4] 196 | value = Value.makeBoolean(True) 197 | msg_round_trip( 198 | Message.entryAssign("Hi", 0x1234, 0x4321, value, 0x00), exclude=exclude 199 | ) 200 | 201 | 202 | def test_wire_entryAssign2(msg_round_trip, proto_rev): 203 | exclude = [] if proto_rev >= 0x0300 else [4] 204 | value = Value.makeString("Oh noes") 205 | msg_round_trip( 206 | Message.entryAssign("Hi", 0x1234, 0x4321, value, 0x00), exclude=exclude 207 | ) 208 | 209 | 210 | def test_wire_entryUpdate1(msg_round_trip, proto_rev): 211 | exclude = [] if proto_rev >= 0x0300 else [4] 212 | value = Value.makeBoolean(True) 213 | msg_round_trip(Message.entryUpdate(0x1234, 0x4321, value), exclude=exclude) 214 | 215 | 216 | def test_wire_entryUpdate2(msg_round_trip, proto_rev): 217 | exclude = [] if proto_rev >= 0x0300 else [4] 218 | value = Value.makeString("Oh noes") 219 | msg_round_trip(Message.entryUpdate(0x1234, 0x4321, value), exclude=exclude) 220 | 221 | 222 | def test_wire_flagsUpdate(msg_round_trip): 223 | msg_round_trip(Message.flagsUpdate(0x1234, 0x42), minver=0x0300) 224 | 225 | 226 | def test_wire_entryDelete(msg_round_trip): 227 | msg_round_trip(Message.entryDelete(0x1234), minver=0x0300) 228 | 229 | 230 | def test_wire_clearEntries(msg_round_trip): 231 | msg_round_trip(Message.clearEntries(), minver=0x0300) 232 | 233 | 234 | def test_wire_executeRpc(msg_round_trip): 235 | msg_round_trip(Message.executeRpc(0x1234, 0x4321, "parameter"), minver=0x0300) 236 | 237 | 238 | def test_wire_rpcResponse(msg_round_trip): 239 | msg_round_trip(Message.rpcResponse(0x1234, 0x4321, "parameter"), minver=0x0300) 240 | 241 | 242 | # Various invalid unicode 243 | def test_decode_invalid_string(proto_rev): 244 | codec = WireCodec(proto_rev) 245 | 246 | if proto_rev == 0x0200: 247 | prefix = b"\x00\x1a" 248 | else: 249 | prefix = b"\x1a" 250 | 251 | fp = BytesIO(prefix + b"\x00\xa7>\x03eWithJoystickCommandV2") 252 | rstream = ReadStream(fp) 253 | 254 | s = "INVALID UTF-8: b'\\x00\\xa7>\\x03eWithJoystickCommandV2'" 255 | 256 | assert codec.read_string(rstream) == s 257 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/RobotPyWPILib.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/RobotPyWPILib.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/RobotPyWPILib" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/RobotPyWPILib" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\RobotPyWPILib.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\RobotPyWPILib.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /_pynetworktables/_impl/wire.py: -------------------------------------------------------------------------------- 1 | # validated: 2017-09-28 DS 5ab20bb27c97 cpp/WireDecoder.cpp cpp/WireDecoder.h cpp/WireEncoder.cpp cpp/WireEncoder.h 2 | """ 3 | This encompasses the WireEncoder and WireDecoder stuff in ntcore 4 | 5 | Reading: 6 | 7 | Writing: 8 | 9 | Each message type will have a write function, which takes 10 | a single list argument. Bytes will be added to that list. 11 | 12 | The write routines assume that the messages are a tuple 13 | that have the following format: 14 | 15 | # This doesn't make sense 16 | type, str, value, id, flags, seqnum 17 | 18 | """ 19 | 20 | import logging 21 | import struct 22 | 23 | from .constants import ( 24 | NT_BOOLEAN, 25 | NT_DOUBLE, 26 | NT_STRING, 27 | NT_RAW, 28 | NT_BOOLEAN_ARRAY, 29 | NT_DOUBLE_ARRAY, 30 | NT_STRING_ARRAY, 31 | NT_RPC, 32 | ) 33 | 34 | from .support import leb128 35 | from .value import Value 36 | 37 | logger = logging.Logger("nt.wire") 38 | 39 | _clientHello = struct.Struct(">H") 40 | _protoUnsup = struct.Struct(">H") 41 | _entryAssignV2 = struct.Struct(">HH") 42 | _entryUpdate = struct.Struct(">HH") 43 | 44 | _serverHello = struct.Struct("b") 45 | _entryAssignV3 = struct.Struct(">HHb") 46 | _flagsUpdate = struct.Struct(">Hb") 47 | _entryDelete = struct.Struct(">H") 48 | _clearEntries = struct.Struct(">I") 49 | _executeRpc = struct.Struct(">HH") 50 | _rpcResponse = struct.Struct(">HH") 51 | 52 | 53 | class WireCodec(object): 54 | 55 | _bool_fmt = struct.Struct("?") 56 | _double_fmt = struct.Struct(">d") 57 | _string_fmt = struct.Struct(">H") 58 | _array_fmt = struct.Struct("B") 59 | _short_fmt = struct.Struct(">H") 60 | 61 | clientHello = _clientHello 62 | protoUnsup = _protoUnsup 63 | entryUpdate = _entryUpdate 64 | 65 | def __init__(self, proto_rev): 66 | self.proto_rev = None 67 | self.set_proto_rev(proto_rev) 68 | 69 | def set_proto_rev(self, proto_rev): 70 | # python-specific optimization 71 | if self.proto_rev == proto_rev: 72 | return 73 | 74 | self.proto_rev = proto_rev 75 | if proto_rev == 0x0200: 76 | self.read_arraylen = self.read_arraylen_v2_v3 77 | self.read_string = self.read_string_v2 78 | self.write_arraylen = self.write_arraylen_v2_v3 79 | self.write_string = self.write_string_v2 80 | 81 | self.entryAssign = _entryAssignV2 82 | 83 | self._del("serverHello") 84 | self._del("flagsUpdate") 85 | self._del("entryDelete") 86 | self._del("clearEntries") 87 | self._del("executeRpc") 88 | self._del("rpcResponse") 89 | 90 | elif proto_rev == 0x0300: 91 | self.read_arraylen = self.read_arraylen_v2_v3 92 | self.read_string = self.read_string_v3 93 | self.write_arraylen = self.write_arraylen_v2_v3 94 | self.write_string = self.write_string_v3 95 | 96 | self.entryAssign = _entryAssignV3 97 | 98 | self.serverHello = _serverHello 99 | self.flagsUpdate = _flagsUpdate 100 | self.entryDelete = _entryDelete 101 | self.clearEntries = _clearEntries 102 | self.executeRpc = _executeRpc 103 | self.rpcResponse = _rpcResponse 104 | 105 | else: 106 | raise ValueError("Unsupported protocol") 107 | 108 | def _del(self, attr): 109 | if hasattr(self, attr): 110 | delattr(self, attr) 111 | 112 | def read_value(self, vtype, rstream): 113 | if vtype == NT_BOOLEAN: 114 | return Value.makeBoolean(rstream.readStruct(self._bool_fmt)[0]) 115 | 116 | elif vtype == NT_DOUBLE: 117 | return Value.makeDouble(rstream.readStruct(self._double_fmt)[0]) 118 | 119 | elif vtype == NT_STRING: 120 | return Value.makeString(self.read_string(rstream)) 121 | 122 | elif vtype == NT_BOOLEAN_ARRAY: 123 | alen = self.read_arraylen(rstream) 124 | return Value.makeBooleanArray( 125 | [rstream.readStruct(self._bool_fmt)[0] for _ in range(alen)] 126 | ) 127 | 128 | elif vtype == NT_DOUBLE_ARRAY: 129 | alen = self.read_arraylen(rstream) 130 | return Value.makeDoubleArray( 131 | [rstream.readStruct(self._double_fmt)[0] for _ in range(alen)] 132 | ) 133 | 134 | elif vtype == NT_STRING_ARRAY: 135 | alen = self.read_arraylen(rstream) 136 | return Value.makeStringArray( 137 | [self.read_string(rstream) for _ in range(alen)] 138 | ) 139 | 140 | elif self.proto_rev >= 0x0300: 141 | if vtype == NT_RAW: 142 | slen = leb128.read_uleb128(rstream) 143 | return Value.makeRaw(rstream.read(slen)) 144 | 145 | elif vtype == NT_RPC: 146 | return Value.makeRpc(self.read_string(rstream)) 147 | 148 | raise ValueError("Cannot decode value type %s" % vtype) 149 | 150 | def write_value(self, v, out): 151 | vtype = v.type 152 | 153 | if vtype == NT_BOOLEAN: 154 | out.append(self._bool_fmt.pack(v.value)) 155 | return 156 | 157 | elif vtype == NT_DOUBLE: 158 | out.append(self._double_fmt.pack(v.value)) 159 | return 160 | 161 | elif vtype == NT_STRING: 162 | self.write_string(v.value, out) 163 | return 164 | 165 | elif vtype == NT_BOOLEAN_ARRAY: 166 | alen = self.write_arraylen(v.value, out) 167 | out += (self._bool_fmt.pack(v) for v in v.value[:alen]) 168 | return 169 | 170 | elif vtype == NT_DOUBLE_ARRAY: 171 | alen = self.write_arraylen(v.value, out) 172 | out += (self._double_fmt.pack(v) for v in v.value[:alen]) 173 | return 174 | 175 | elif vtype == NT_STRING_ARRAY: 176 | alen = self.write_arraylen(v.value, out) 177 | for s in v.value[:alen]: 178 | self.write_string(s, out) 179 | return 180 | 181 | elif self.proto_rev >= 0x0300: 182 | if vtype == NT_RPC: 183 | self.write_string(v.value, out) 184 | return 185 | 186 | elif vtype == NT_RAW: 187 | s = v.value 188 | out += (leb128.encode_uleb128(len(s)), s) 189 | return 190 | 191 | raise ValueError("Cannot encode invalid value type %s" % vtype) 192 | 193 | # 194 | # v2/v3 routines 195 | # 196 | 197 | def read_arraylen_v2_v3(self, rstream): 198 | return rstream.readStruct(self._array_fmt)[0] 199 | 200 | # v4 perhaps 201 | # def read_arraylen_v3(self, rstream): 202 | # return leb128.read_uleb128(rstream) 203 | 204 | def read_string_v2(self, rstream): 205 | slen = rstream.readStruct(self._string_fmt)[0] 206 | b = rstream.read(slen) 207 | try: 208 | return b.decode("utf-8") 209 | except UnicodeDecodeError: 210 | logger.warning("Received an invalid UTF-8 string: %r", b) 211 | return "INVALID UTF-8: %r" % b 212 | 213 | def read_string_v3(self, rstream): 214 | slen = leb128.read_uleb128(rstream) 215 | b = rstream.read(slen) 216 | try: 217 | return b.decode("utf-8") 218 | except UnicodeDecodeError: 219 | logger.warning("Received an invalid UTF-8 string: %r", b) 220 | return "INVALID UTF-8: %r" % b 221 | 222 | def write_arraylen_v2_v3(self, a, out): 223 | alen = min(len(a), 0xFF) 224 | out.append(self._array_fmt.pack(alen)) 225 | return alen 226 | 227 | # v4 perhaps 228 | # def write_arraylen_v3(self, a, out): 229 | # alen = len(a) 230 | # out.append(leb128.encode_uleb128(alen)) 231 | # return alen 232 | 233 | def write_string_v2(self, s, out): 234 | s = s.encode("utf-8") 235 | out += (self._string_fmt.pack(min(len(s), 0xFFFF)), s[:0xFFFF]) 236 | 237 | def write_string_v3(self, s, out): 238 | s = s.encode("utf-8") 239 | out += (leb128.encode_uleb128(len(s)), s) 240 | -------------------------------------------------------------------------------- /_pynetworktables/_impl/message.py: -------------------------------------------------------------------------------- 1 | # validated: 2018-01-06 DV 2287281066f6 cpp/Message.cpp cpp/Message.h 2 | 3 | from collections import namedtuple 4 | 5 | from .constants import ( 6 | kKeepAlive, 7 | kClientHello, 8 | kProtoUnsup, 9 | kServerHello, 10 | kServerHelloDone, 11 | kClientHelloDone, 12 | kEntryAssign, 13 | kEntryUpdate, 14 | kFlagsUpdate, 15 | kEntryDelete, 16 | kClearEntries, 17 | kExecuteRpc, 18 | kRpcResponse, 19 | kClearAllMagic, 20 | NT_VTYPE2RAW, 21 | NT_RAW2VTYPE, 22 | ) 23 | 24 | 25 | class Message( 26 | namedtuple("Message", ["type", "str", "value", "id", "flags", "seq_num_uid"]) 27 | ): 28 | __slots__ = () 29 | 30 | _empty_msgtypes = (kKeepAlive, kServerHelloDone, kClientHelloDone) 31 | 32 | @classmethod 33 | def keepAlive(cls): 34 | return cls(kKeepAlive, None, None, None, None, None) 35 | 36 | @classmethod 37 | def clientHello(cls, proto_rev, identity): 38 | return cls(kClientHello, identity, None, proto_rev, None, None) 39 | 40 | @classmethod 41 | def protoUnsup(cls, proto_rev): 42 | return cls(kProtoUnsup, None, None, proto_rev, None, None) 43 | 44 | @classmethod 45 | def serverHelloDone(cls): 46 | return cls(kServerHelloDone, None, None, None, None, None) 47 | 48 | @classmethod 49 | def serverHello(cls, flags, identity): 50 | return cls(kServerHello, identity, None, None, flags, None) 51 | 52 | @classmethod 53 | def clientHelloDone(cls): 54 | return cls(kClientHelloDone, None, None, None, None, None) 55 | 56 | @classmethod 57 | def entryAssign(cls, name, msg_id, seq_num_uid, value, flags): 58 | return cls(kEntryAssign, name, value, msg_id, flags, seq_num_uid) 59 | 60 | @classmethod 61 | def entryUpdate(cls, entry_id, seq_num_uid, value): 62 | return cls(kEntryUpdate, None, value, entry_id, None, seq_num_uid) 63 | 64 | @classmethod 65 | def flagsUpdate(cls, msg_id, flags): 66 | return cls(kFlagsUpdate, None, None, msg_id, flags, None) 67 | 68 | @classmethod 69 | def entryDelete(cls, entry_id): 70 | return cls(kEntryDelete, None, None, entry_id, None, None) 71 | 72 | @classmethod 73 | def clearEntries(cls): 74 | return cls(kClearEntries, None, None, kClearAllMagic, None, None) 75 | 76 | @classmethod 77 | def executeRpc(cls, rpc_id, call_uid, params): 78 | return cls(kExecuteRpc, params, None, rpc_id, None, call_uid) 79 | 80 | @classmethod 81 | def rpcResponse(cls, rpc_id, call_uid, result): 82 | return cls(kRpcResponse, result, None, rpc_id, None, call_uid) 83 | 84 | @classmethod 85 | def read(cls, rstream, codec, get_entry_type) -> "Message": 86 | msgtype = rstream.read(1) 87 | 88 | msg_str = None 89 | value = None 90 | msg_id = None 91 | flags = None 92 | seq_num_uid = None 93 | 94 | # switch type 95 | if msgtype in cls._empty_msgtypes: 96 | pass 97 | 98 | # python optimization: entry updates tend to occur more than 99 | # anything else, so check this first 100 | elif msgtype == kEntryUpdate: 101 | if codec.proto_rev >= 0x0300: 102 | msg_id, seq_num_uid = rstream.readStruct(codec.entryUpdate) 103 | value_type = NT_RAW2VTYPE.get(rstream.read(1)) 104 | else: 105 | msg_id, seq_num_uid = rstream.readStruct(codec.entryUpdate) 106 | value_type = get_entry_type(msg_id) 107 | 108 | value = codec.read_value(value_type, rstream) 109 | 110 | elif msgtype == kClientHello: 111 | (msg_id,) = rstream.readStruct(codec.clientHello) 112 | if msg_id >= 0x0300: 113 | msg_str = codec.read_string_v3(rstream) 114 | 115 | elif msgtype == kProtoUnsup: 116 | (msg_id,) = rstream.readStruct(codec.protoUnsup) 117 | 118 | elif msgtype == kServerHello: 119 | (flags,) = rstream.readStruct(codec.serverHello) 120 | msg_str = codec.read_string(rstream) 121 | 122 | elif msgtype == kEntryAssign: 123 | msg_str = codec.read_string(rstream) 124 | value_type = NT_RAW2VTYPE.get(rstream.read(1)) 125 | if codec.proto_rev >= 0x0300: 126 | msg_id, seq_num_uid, flags = rstream.readStruct(codec.entryAssign) 127 | else: 128 | msg_id, seq_num_uid = rstream.readStruct(codec.entryAssign) 129 | flags = 0 130 | 131 | value = codec.read_value(value_type, rstream) 132 | 133 | elif msgtype == kFlagsUpdate: 134 | msg_id, flags = rstream.readStruct(codec.flagsUpdate) 135 | 136 | elif msgtype == kEntryDelete: 137 | (msg_id,) = rstream.readStruct(codec.entryDelete) 138 | 139 | elif msgtype == kClearEntries: 140 | (msg_id,) = rstream.readStruct(codec.clearEntries) 141 | if msg_id != kClearAllMagic: 142 | raise ValueError("Bad magic") 143 | 144 | elif msgtype == kExecuteRpc: 145 | msg_id, seq_num_uid = rstream.readStruct(codec.executeRpc) 146 | msg_str = codec.read_string(rstream) 147 | 148 | elif msgtype == kRpcResponse: 149 | msg_id, seq_num_uid = rstream.readStruct(codec.rpcResponse) 150 | msg_str = codec.read_string(rstream) 151 | 152 | else: 153 | raise ValueError("Unrecognized message type %s" % msgtype) 154 | 155 | return cls(msgtype, msg_str, value, msg_id, flags, seq_num_uid) 156 | 157 | def write(self, out, codec): 158 | msgtype = self.type 159 | 160 | # switch type 161 | if msgtype in self._empty_msgtypes: 162 | out.append(msgtype) 163 | 164 | elif msgtype == kClientHello: 165 | proto_rev = self.id 166 | out += (msgtype, codec.clientHello.pack(proto_rev)) 167 | 168 | if proto_rev >= 0x0300: 169 | codec.write_string_v3(self.str, out) 170 | 171 | elif msgtype == kProtoUnsup: 172 | out += (msgtype, codec.protoUnsup.pack(self.id)) 173 | 174 | elif msgtype == kServerHello: 175 | if codec.proto_rev >= 0x0300: 176 | out += (msgtype, codec.serverHello.pack(self.flags)) 177 | codec.write_string(self.str, out) 178 | 179 | elif msgtype == kEntryAssign: 180 | out.append(msgtype) 181 | codec.write_string(self.str, out) 182 | 183 | value = self.value 184 | if codec.proto_rev >= 0x0300: 185 | sb = codec.entryAssign.pack(self.id, self.seq_num_uid, self.flags) 186 | else: 187 | sb = codec.entryAssign.pack(self.id, self.seq_num_uid) 188 | out += (NT_VTYPE2RAW[value.type], sb) 189 | 190 | codec.write_value(value, out) 191 | 192 | elif msgtype == kEntryUpdate: 193 | value = self.value 194 | if codec.proto_rev >= 0x0300: 195 | out += ( 196 | msgtype, 197 | codec.entryUpdate.pack(self.id, self.seq_num_uid), 198 | NT_VTYPE2RAW[value.type], 199 | ) 200 | else: 201 | out += (msgtype, codec.entryUpdate.pack(self.id, self.seq_num_uid)) 202 | 203 | codec.write_value(value, out) 204 | 205 | elif msgtype == kFlagsUpdate: 206 | if codec.proto_rev >= 0x0300: 207 | out += (msgtype, codec.flagsUpdate.pack(self.id, self.flags)) 208 | 209 | elif msgtype == kEntryDelete: 210 | if codec.proto_rev >= 0x0300: 211 | out += (msgtype, codec.entryDelete.pack(self.id)) 212 | 213 | elif msgtype == kClearEntries: 214 | if codec.proto_rev >= 0x0300: 215 | out += (msgtype, codec.clearEntries.pack(self.id)) 216 | 217 | elif msgtype == kExecuteRpc: 218 | if codec.proto_rev >= 0x0300: 219 | out += (msgtype, codec.executeRpc.pack(self.id, self.seq_num_uid)) 220 | codec.write_string(self.str, out) 221 | 222 | elif msgtype == kRpcResponse: 223 | if codec.proto_rev >= 0x0300: 224 | out += (msgtype, codec.rpcResponse.pack(self.id, self.seq_num_uid)) 225 | codec.write_string(self.str, out) 226 | 227 | else: 228 | raise ValueError("Internal error: bad value type %s" % self.type) 229 | -------------------------------------------------------------------------------- /tests/test_network_table_listener.py: -------------------------------------------------------------------------------- 1 | # 2 | # These tests are leftover from the original pynetworktables tests 3 | # 4 | 5 | from unittest.mock import call, Mock 6 | import pytest 7 | 8 | from _pynetworktables import NetworkTables 9 | 10 | 11 | @pytest.fixture(scope="function") 12 | def table1(nt): 13 | return nt.getTable("/test1") 14 | 15 | 16 | @pytest.fixture(scope="function") 17 | def table2(nt): 18 | return nt.getTable("/test2") 19 | 20 | 21 | @pytest.fixture(scope="function") 22 | def table3(nt): 23 | return nt.getTable("/test3") 24 | 25 | 26 | @pytest.fixture(scope="function") 27 | def subtable1(nt): 28 | return nt.getTable("/test2/sub1") 29 | 30 | 31 | @pytest.fixture(scope="function") 32 | def subtable2(nt): 33 | return nt.getTable("/test2/sub2") 34 | 35 | 36 | @pytest.fixture(scope="function") 37 | def subtable3(nt): 38 | return nt.getTable("/test3/suba") 39 | 40 | 41 | @pytest.fixture(scope="function") 42 | def subtable4(nt): 43 | return nt.getTable("/test3/suba/subb") 44 | 45 | 46 | def test_key_listener_immediate_notify(table1, nt_flush): 47 | 48 | listener1 = Mock() 49 | 50 | table1.putBoolean("MyKey1", True) 51 | table1.putBoolean("MyKey1", False) 52 | table1.putBoolean("MyKey2", True) 53 | table1.putBoolean("MyKey4", False) 54 | nt_flush() 55 | 56 | table1.addEntryListener(listener1.valueChanged, True, localNotify=True) 57 | 58 | nt_flush() 59 | listener1.valueChanged.assert_has_calls( 60 | [ 61 | call(table1, "MyKey1", False, True), 62 | call(table1, "MyKey2", True, True), 63 | call(table1, "MyKey4", False, True), 64 | ], 65 | True, 66 | ) 67 | assert len(listener1.mock_calls) == 3 68 | listener1.reset_mock() 69 | 70 | table1.putBoolean("MyKey", False) 71 | nt_flush() 72 | listener1.valueChanged.assert_called_once_with(table1, "MyKey", False, True) 73 | assert len(listener1.mock_calls) == 1 74 | listener1.reset_mock() 75 | 76 | table1.putBoolean("MyKey1", True) 77 | nt_flush() 78 | listener1.valueChanged.assert_called_once_with(table1, "MyKey1", True, False) 79 | assert len(listener1.mock_calls) == 1 80 | listener1.reset_mock() 81 | 82 | table1.putBoolean("MyKey1", False) 83 | nt_flush() 84 | listener1.valueChanged.assert_called_once_with(table1, "MyKey1", False, False) 85 | assert len(listener1.mock_calls) == 1 86 | listener1.reset_mock() 87 | 88 | table1.putBoolean("MyKey4", True) 89 | nt_flush() 90 | listener1.valueChanged.assert_called_once_with(table1, "MyKey4", True, False) 91 | assert len(listener1.mock_calls) == 1 92 | listener1.reset_mock() 93 | 94 | 95 | def test_key_listener_not_immediate_notify(table1, nt_flush): 96 | 97 | listener1 = Mock() 98 | 99 | table1.putBoolean("MyKey1", True) 100 | table1.putBoolean("MyKey1", False) 101 | table1.putBoolean("MyKey2", True) 102 | table1.putBoolean("MyKey4", False) 103 | 104 | table1.addEntryListener(listener1.valueChanged, False, localNotify=True) 105 | assert len(listener1.mock_calls) == 0 106 | listener1.reset_mock() 107 | 108 | table1.putBoolean("MyKey", False) 109 | nt_flush() 110 | listener1.valueChanged.assert_called_once_with(table1, "MyKey", False, True) 111 | assert len(listener1.mock_calls) == 1 112 | listener1.reset_mock() 113 | 114 | table1.putBoolean("MyKey1", True) 115 | nt_flush() 116 | listener1.valueChanged.assert_called_once_with(table1, "MyKey1", True, False) 117 | assert len(listener1.mock_calls) == 1 118 | listener1.reset_mock() 119 | 120 | table1.putBoolean("MyKey1", False) 121 | nt_flush() 122 | listener1.valueChanged.assert_called_once_with(table1, "MyKey1", False, False) 123 | assert len(listener1.mock_calls) == 1 124 | listener1.reset_mock() 125 | 126 | table1.putBoolean("MyKey4", True) 127 | nt_flush() 128 | listener1.valueChanged.assert_called_once_with(table1, "MyKey4", True, False) 129 | assert len(listener1.mock_calls) == 1 130 | listener1.reset_mock() 131 | 132 | 133 | def test_specific_key_listener(table1, nt_flush): 134 | 135 | listener1 = Mock() 136 | 137 | table1.addEntryListener( 138 | listener1.valueChanged, False, key="MyKey1", localNotify=True 139 | ) 140 | nt_flush() 141 | assert len(listener1.mock_calls) == 0 142 | 143 | table1.putBoolean("MyKey1", True) 144 | nt_flush() 145 | listener1.valueChanged.assert_called_once_with(table1, "MyKey1", True, True) 146 | assert len(listener1.mock_calls) == 1 147 | listener1.reset_mock() 148 | 149 | table1.putBoolean("MyKey2", True) 150 | nt_flush() 151 | assert len(listener1.mock_calls) == 0 152 | 153 | 154 | def test_specific_entry_listener(table1, nt_flush): 155 | 156 | listener1 = Mock() 157 | NotifyFlags = NetworkTables.NotifyFlags 158 | 159 | entry = table1.getEntry("MyKey1") 160 | entry.addListener( 161 | listener1.valueChanged, NotifyFlags.NEW | NotifyFlags.UPDATE | NotifyFlags.LOCAL 162 | ) 163 | nt_flush() 164 | assert len(listener1.mock_calls) == 0 165 | 166 | table1.putBoolean("MyKey1", True) 167 | nt_flush() 168 | listener1.valueChanged.assert_called_once_with(entry, "/test1/MyKey1", True, True) 169 | assert len(listener1.mock_calls) == 1 170 | listener1.reset_mock() 171 | 172 | table1.putBoolean("MyKey2", True) 173 | nt_flush() 174 | assert len(listener1.mock_calls) == 0 175 | 176 | 177 | def test_subtable_listener(table2, subtable1, subtable2, nt_flush): 178 | 179 | listener1 = Mock() 180 | 181 | table2.putBoolean("MyKey1", True) 182 | table2.putBoolean("MyKey1", False) 183 | table2.addSubTableListener(listener1.valueChanged, localNotify=True) 184 | table2.putBoolean("MyKey2", True) 185 | table2.putBoolean("MyKey4", False) 186 | 187 | subtable1.putBoolean("MyKey1", False) 188 | 189 | nt_flush() 190 | listener1.valueChanged.assert_called_once_with(table2, "sub1", subtable1, True) 191 | assert len(listener1.mock_calls) == 1 192 | listener1.reset_mock() 193 | 194 | subtable1.putBoolean("MyKey2", True) 195 | subtable1.putBoolean("MyKey1", True) 196 | subtable2.putBoolean("MyKey1", False) 197 | 198 | nt_flush() 199 | listener1.valueChanged.assert_called_once_with(table2, "sub2", subtable2, True) 200 | assert len(listener1.mock_calls) == 1 201 | listener1.reset_mock() 202 | 203 | 204 | def test_subsubtable_listener(table3, subtable3, subtable4, nt_flush): 205 | listener1 = Mock() 206 | 207 | table3.addSubTableListener(listener1.valueChanged, localNotify=True) 208 | subtable3.addSubTableListener(listener1.valueChanged, localNotify=True) 209 | subtable4.addEntryListener(listener1.valueChanged, True, localNotify=True) 210 | 211 | subtable4.putBoolean("MyKey1", False) 212 | 213 | nt_flush() 214 | listener1.valueChanged.assert_has_calls( 215 | [ 216 | call(table3, "suba", subtable3, True), 217 | call(subtable3, "subb", subtable4, True), 218 | call(subtable4, "MyKey1", False, True), 219 | ], 220 | True, 221 | ) 222 | assert len(listener1.mock_calls) == 3 223 | listener1.reset_mock() 224 | 225 | subtable4.putBoolean("MyKey1", True) 226 | 227 | nt_flush() 228 | listener1.valueChanged.assert_called_once_with(subtable4, "MyKey1", True, False) 229 | assert len(listener1.mock_calls) == 1 230 | listener1.reset_mock() 231 | 232 | listener2 = Mock() 233 | 234 | table3.addSubTableListener(listener2.valueChanged, localNotify=True) 235 | subtable3.addSubTableListener(listener2.valueChanged, localNotify=True) 236 | subtable4.addEntryListener(listener2.valueChanged, True, localNotify=True) 237 | 238 | nt_flush() 239 | listener2.valueChanged.assert_has_calls( 240 | [ 241 | call(table3, "suba", subtable3, True), 242 | call(subtable3, "subb", subtable4, True), 243 | call(subtable4, "MyKey1", True, True), 244 | ], 245 | True, 246 | ) 247 | assert len(listener1.mock_calls) == 0 248 | assert len(listener2.mock_calls) == 3 249 | listener2.reset_mock() 250 | 251 | 252 | def test_global_listener(nt, nt_flush, table1, subtable3): 253 | listener = Mock() 254 | 255 | nt.addEntryListener(listener) 256 | listener.assert_not_called() 257 | 258 | table1.putString("t1", "hi") 259 | subtable3.putString("tt", "y0") 260 | nt_flush() 261 | listener.assert_has_calls( 262 | [call("/test1/t1", "hi", True), call("/test3/suba/tt", "y0", True)] 263 | ) 264 | listener.reset_mock() 265 | 266 | nt.removeEntryListener(listener) 267 | 268 | table1.putString("s", "1") 269 | nt_flush() 270 | listener.assert_not_called() 271 | -------------------------------------------------------------------------------- /_pynetworktables/_impl/api.py: -------------------------------------------------------------------------------- 1 | # validated: 2018-11-27 DS 8eafe7f32561 cpp/ntcore_cpp.cpp 2 | 3 | from .connection_notifier import ConnectionNotifier 4 | from .dispatcher import Dispatcher 5 | from .ds_client import DsClient 6 | from .entry_notifier import EntryNotifier 7 | from .rpc_server import RpcServer 8 | from .storage import Storage 9 | 10 | from .constants import NT_NOTIFY_IMMEDIATE, NT_NOTIFY_NEW 11 | 12 | _is_new = NT_NOTIFY_IMMEDIATE | NT_NOTIFY_NEW 13 | 14 | 15 | class NtCoreApi(object): 16 | """ 17 | Internal NetworkTables API wrapper 18 | 19 | In theory you could create multiple instances of this 20 | and talk to multiple NT servers or create multiple 21 | NT servers... though, I don't really know why one 22 | would want to do this. 23 | """ 24 | 25 | def __init__(self, entry_creator, verbose=False): 26 | self.conn_notifier = ConnectionNotifier(verbose=verbose) 27 | self.entry_notifier = EntryNotifier(verbose=verbose) 28 | self.rpc_server = RpcServer(verbose=verbose) 29 | self.storage = Storage(self.entry_notifier, self.rpc_server, entry_creator) 30 | self.dispatcher = Dispatcher(self.storage, self.conn_notifier, verbose=verbose) 31 | self.ds_client = DsClient(self.dispatcher, verbose=verbose) 32 | 33 | self._init_table_functions() 34 | 35 | def stop(self): 36 | self.ds_client.stop() 37 | self.dispatcher.stop() 38 | self.rpc_server.stop() 39 | self.entry_notifier.stop() 40 | self.conn_notifier.stop() 41 | self.storage.stop() 42 | 43 | def destroy(self): 44 | self.ds_client = None 45 | self.dispatcher = None 46 | self.rpc_server = None 47 | self.entry_notifier = None 48 | self.entry_notifier = None 49 | self.conn_notifier = None 50 | self.storage = None 51 | 52 | # 53 | # Table functions (inline because they're called often) 54 | # 55 | 56 | def _init_table_functions(self): 57 | self.getEntry = self.storage.getEntry 58 | self.getEntryId = self.storage.getEntryId 59 | self.getEntries = self.storage.getEntries 60 | self.getEntryNameById = self.storage.getEntryNameById 61 | self.getEntryTypeById = self.storage.getEntryTypeById 62 | self.getEntryValue = self.storage.getEntryValue 63 | self.setDefaultEntryValue = self.storage.setDefaultEntryValue 64 | self.setDefaultEntryValueById = self.storage.setDefaultEntryValueById 65 | self.setEntryValue = self.storage.setEntryValue 66 | self.setEntryValueById = self.storage.setEntryValueById 67 | self.setEntryTypeValue = self.storage.setEntryTypeValue 68 | self.setEntryTypeValueById = self.storage.setEntryTypeValueById 69 | self.setEntryFlags = self.storage.setEntryFlags 70 | self.setEntryFlagsById = self.storage.setEntryFlagsById 71 | self.getEntryFlags = self.storage.getEntryFlags 72 | self.getEntryFlagsById = self.storage.getEntryFlagsById 73 | self.deleteEntry = self.storage.deleteEntry 74 | self.deleteEntryById = self.storage.deleteEntryById 75 | self.deleteAllEntries = self.storage.deleteAllEntries 76 | self.getEntryInfo = self.storage.getEntryInfo 77 | self.getEntryInfoById = self.storage.getEntryInfoById 78 | 79 | # 80 | # Entry notification 81 | # 82 | 83 | def addEntryListener(self, prefix, callback, flags): 84 | return self.storage.addListener(prefix, callback, flags) 85 | 86 | def addEntryListenerById(self, local_id, callback, flags): 87 | return self.storage.addListenerById(local_id, callback, flags) 88 | 89 | def addEntryListenerByIdEx( 90 | self, fromobj, key, local_id, callback, flags, paramIsNew 91 | ): 92 | if paramIsNew: 93 | 94 | def listener(item): 95 | key_, value_, flags_, _ = item 96 | callback(fromobj, key, value_.value, (flags_ & _is_new) != 0) 97 | 98 | else: 99 | 100 | def listener(item): 101 | key_, value_, flags_, _ = item 102 | callback(fromobj, key, value_.value, flags_) 103 | 104 | return self.storage.addListenerById(local_id, listener, flags) 105 | 106 | def createEntryListenerPoller(self): 107 | return self.entry_notifier.createPoller() 108 | 109 | def destroyEntryListenerPoller(self, poller_uid): 110 | self.entry_notifier.removePoller(poller_uid) 111 | 112 | def addPolledEntryListener(self, poller_uid, prefix, flags): 113 | return self.storage.addPolledListener(poller_uid, prefix, flags) 114 | 115 | def addPolledEntryListenerById(self, poller_uid, local_id, flags): 116 | return self.storage.addPolledListenerById(poller_uid, local_id, flags) 117 | 118 | def pollEntryListener(self, poller_uid, timeout=None): 119 | return self.entry_notifier.poll(poller_uid, timeout=timeout) 120 | 121 | def cancelPollEntryListener(self, poller_uid): 122 | self.entry_notifier.cancelPoll(poller_uid) 123 | 124 | def removeEntryListener(self, listener_uid): 125 | self.entry_notifier.remove(listener_uid) 126 | 127 | def waitForEntryListenerQueue(self, timeout): 128 | return self.entry_notifier.waitForQueue(timeout) 129 | 130 | # 131 | # Connection notifications 132 | # 133 | 134 | def addConnectionListener(self, callback, immediate_notify): 135 | return self.dispatcher.addListener(callback, immediate_notify) 136 | 137 | def createConnectionListenerPoller(self): 138 | return self.conn_notifier.createPoller() 139 | 140 | def destroyConnectionListenerPoller(self, poller_uid): 141 | self.conn_notifier.removePoller(poller_uid) 142 | 143 | def addPolledConnectionListener(self, poller_uid, immediate_notify): 144 | return self.dispatcher.addPolledListener(poller_uid, immediate_notify) 145 | 146 | def pollConnectionListener(self, poller_uid, timeout=None): 147 | return self.conn_notifier.poll(poller_uid, timeout=timeout) 148 | 149 | def cancelPollConnectionListener(self, poller_uid): 150 | self.conn_notifier.cancelPoll(poller_uid) 151 | 152 | def removeConnectionListener(self, listener_uid): 153 | self.conn_notifier.remove(listener_uid) 154 | 155 | def waitForConnectionListenerQueue(self, timeout): 156 | return self.conn_notifier.waitForQueue(timeout) 157 | 158 | # 159 | # TODO: RPC stuff not currently implemented 160 | # .. there's probably a good pythonic way to implement 161 | # it, but I don't really want to deal with it now. 162 | # If you care, submit a PR. 163 | # 164 | # I would have the caller register the server function 165 | # via a docstring. 166 | # 167 | 168 | # 169 | # Client/Server Functions 170 | # 171 | 172 | def setNetworkIdentity(self, name): 173 | self.dispatcher.setIdentity(name) 174 | 175 | def getNetworkMode(self): 176 | return self.dispatcher.getNetworkMode() 177 | 178 | # python-specific 179 | def startTestMode(self, is_server): 180 | if self.dispatcher.startTestMode(is_server): 181 | self.storage.m_server = is_server 182 | return True 183 | else: 184 | return False 185 | 186 | def startServer(self, persist_filename, listen_address, port): 187 | return self.dispatcher.startServer(persist_filename, listen_address, port) 188 | 189 | def stopServer(self): 190 | self.dispatcher.stop() 191 | 192 | def startClient(self): 193 | return self.dispatcher.startClient() 194 | 195 | def stopClient(self): 196 | self.dispatcher.stop() 197 | 198 | def setServer(self, server_or_servers): 199 | self.dispatcher.setServer(server_or_servers) 200 | 201 | def setServerTeam(self, teamNumber, port): 202 | self.dispatcher.setServerTeam(teamNumber, port) 203 | 204 | def startDSClient(self, port): 205 | self.ds_client.start(port) 206 | 207 | def stopDSClient(self): 208 | self.ds_client.stop() 209 | 210 | def setUpdateRate(self, interval): 211 | self.dispatcher.setUpdateRate(interval) 212 | 213 | def flush(self): 214 | self.dispatcher.flush() 215 | 216 | def getRemoteAddress(self): 217 | if not self.dispatcher.isServer(): 218 | for conn in self.dispatcher.getConnections(): 219 | return conn.remote_ip 220 | 221 | def getIsConnected(self): 222 | return self.dispatcher.isConnected() 223 | 224 | def setVerboseLogging(self, verbose): 225 | self.conn_notifier.setVerboseLogging(verbose) 226 | self.dispatcher.setVerboseLogging(verbose) 227 | self.entry_notifier.setVerboseLogging(verbose) 228 | self.rpc_server.setVerboseLogging(verbose) 229 | 230 | # 231 | # Persistence 232 | # 233 | 234 | def savePersistent(self, filename): 235 | return self.storage.savePersistent(filename, periodic=False) 236 | 237 | def loadPersistent(self, filename): 238 | return self.storage.loadPersistent(filename) 239 | 240 | def saveEntries(self, filename, prefix): 241 | return self.storage.saveEntries(prefix, filename=filename) 242 | 243 | def loadEntries(self, filename, prefix): 244 | return self.storage.loadEntries(filename=filename, prefix=prefix) 245 | -------------------------------------------------------------------------------- /_pynetworktables/_impl/callback_manager.py: -------------------------------------------------------------------------------- 1 | # validated: 2018-11-27 DS 18c8cce6a78d cpp/CallbackManager.h 2 | # ---------------------------------------------------------------------------- 3 | # Copyright (c) FIRST 2017. All Rights Reserved. 4 | # Open Source Software - may be modified and shared by FRC teams. The code 5 | # must be accompanied by the FIRST BSD license file in the root directory of 6 | # the project. 7 | # ---------------------------------------------------------------------------- 8 | 9 | from collections import deque, namedtuple 10 | from threading import Condition 11 | import time 12 | 13 | try: 14 | # Python 3.7 only, should be more efficient 15 | from queue import SimpleQueue as Queue, Empty 16 | except ImportError: 17 | from queue import Queue, Empty 18 | 19 | from .support.safe_thread import SafeThread 20 | from .support.uidvector import UidVector 21 | 22 | import logging 23 | 24 | logger = logging.getLogger("nt") 25 | 26 | _ListenerData = namedtuple("ListenerData", ["callback", "poller_uid"]) 27 | 28 | 29 | class Poller(object): 30 | def __init__(self): 31 | # Note: this is really close to the python queue, but we really have to 32 | # mess with its internals to get the same semantics as WPILib, so we are 33 | # rolling our own :( 34 | self.poll_queue = deque() 35 | self.poll_cond = Condition() 36 | self.terminating = False 37 | self.cancelling = False 38 | 39 | def terminate(self): 40 | with self.poll_cond: 41 | self.terminating = True 42 | self.poll_cond.notify_all() 43 | 44 | 45 | class CallbackThread(object): 46 | def __init__(self, name): 47 | # Don't need this in python, queue already has one 48 | # self.m_mutex = threading.Lock() 49 | 50 | self.m_listeners = UidVector() 51 | self.m_queue = Queue() 52 | self.m_pollers = UidVector() 53 | 54 | self.m_active = False 55 | self.name = name 56 | 57 | # 58 | # derived must implement the following 59 | # 60 | 61 | def matches(self, listener, data): 62 | raise NotImplementedError 63 | 64 | def setListener(self, data, listener_uid): 65 | raise NotImplementedError 66 | 67 | def doCallback(self, callback, data): 68 | raise NotImplementedError 69 | 70 | # 71 | # Impl 72 | # 73 | 74 | def start(self): 75 | self.m_active = True 76 | self._thread = SafeThread(target=self.main, name=self.name) 77 | 78 | def stop(self): 79 | self.m_active = False 80 | self.m_queue.put(None) 81 | 82 | def sendPoller(self, poller_uid, *args): 83 | # args are (listener_uid, item) 84 | poller = self.m_pollers.get(poller_uid) 85 | if poller: 86 | with poller.poll_cond: 87 | poller.poll_queue.append(args) 88 | poller.poll_cond.notify() 89 | 90 | def main(self): 91 | # micro-optimization: lift these out of the loop 92 | doCallback = self.doCallback 93 | matches = self.matches 94 | queue_get = self.m_queue.get 95 | setListener = self.setListener 96 | listeners_get = self.m_listeners.get 97 | listeners_items = self.m_listeners.items 98 | 99 | while True: 100 | item = queue_get() 101 | if not item: 102 | logger.debug("%s thread no longer active", self.name) 103 | break 104 | 105 | listener_uid, item = item 106 | if listener_uid is not None: 107 | listener = listeners_get(listener_uid) 108 | if listener and matches(listener, item): 109 | setListener(item, listener_uid) 110 | cb = listener.callback 111 | if cb: 112 | try: 113 | doCallback(cb, item) 114 | except Exception: 115 | logger.warning( 116 | "Unhandled exception processing %s callback", 117 | self.name, 118 | exc_info=True, 119 | ) 120 | elif listener.poller_uid is not None: 121 | self.sendPoller(listener.poller_uid, listener_uid, item) 122 | else: 123 | # Use copy because iterator might get invalidated 124 | for listener_uid, listener in list(listeners_items()): 125 | if matches(listener, item): 126 | setListener(item, listener_uid) 127 | cb = listener.callback 128 | if cb: 129 | try: 130 | doCallback(cb, item) 131 | except Exception: 132 | logger.warning( 133 | "Unhandled exception processing %s callback", 134 | self.name, 135 | exc_info=True, 136 | ) 137 | elif listener.poller_uid is not None: 138 | self.sendPoller(listener.poller_uid, listener_uid, item) 139 | 140 | # Wake any blocked pollers 141 | for poller in self.m_pollers.values(): 142 | poller.terminate() 143 | 144 | 145 | class CallbackManager(object): 146 | 147 | # Derived classes should declare this attribute at class level: 148 | # THREAD_CLASS = Something 149 | 150 | def __init__(self, verbose): 151 | self.m_verbose = verbose 152 | self.m_owner = None 153 | 154 | def setVerboseLogging(self, verbose): 155 | self.m_verbose = verbose 156 | 157 | def stop(self): 158 | if self.m_owner: 159 | self.m_owner.stop() 160 | 161 | def remove(self, listener_uid): 162 | thr = self.m_owner 163 | if thr: 164 | thr.m_listeners.pop(listener_uid, None) 165 | 166 | def createPoller(self): 167 | self.start() 168 | thr = self.m_owner 169 | return thr.m_pollers.add(Poller()) 170 | 171 | def removePoller(self, poller_uid): 172 | thr = self.m_owner 173 | if not thr: 174 | return 175 | 176 | # Remove any listeners that are associated with this poller 177 | listeners = list(thr.m_listeners.items()) 178 | for lid, listener in listeners: 179 | if listener.poller_uid == poller_uid: 180 | thr.m_listeners.pop(lid) 181 | 182 | # Wake up any blocked pollers 183 | poller = thr.m_pollers.pop(poller_uid, None) 184 | if not poller: 185 | return 186 | 187 | poller.terminate() 188 | 189 | def waitForQueue(self, timeout): 190 | thr = self.m_owner 191 | if not thr: 192 | return True 193 | 194 | # This function is intended for unit testing purposes only, so it's 195 | # not as efficient as it could be 196 | q = thr.m_queue 197 | 198 | if timeout is None: 199 | while not q.empty() and thr.m_active: 200 | time.sleep(0.005) 201 | else: 202 | wait_until = time.monotonic() + timeout 203 | while not q.empty() and thr.m_active: 204 | time.sleep(0.005) 205 | if time.monotonic() > wait_until: 206 | return q.empty() 207 | 208 | return True 209 | 210 | def poll(self, poller_uid, timeout=None): 211 | # returns infos, timed_out 212 | # -> infos is a list of (listener_uid, item) 213 | infos = [] 214 | timed_out = False 215 | 216 | thr = self.m_owner 217 | if not thr: 218 | return infos, timed_out 219 | 220 | poller = thr.m_pollers.get(poller_uid) 221 | if not poller: 222 | return infos, timed_out 223 | 224 | def _poll_fn(): 225 | if poller.poll_queue: 226 | return 1 227 | if poller.cancelling: 228 | # Note: this only works if there's a single thread calling this 229 | # function for any particular poller, but that's the intended use. 230 | poller.cancelling = False 231 | return 2 232 | 233 | with poller.poll_cond: 234 | result = poller.poll_cond.wait_for(_poll_fn, timeout) 235 | if result is None: # timeout 236 | timed_out = True 237 | elif result == 1: # success 238 | infos.extend(poller.poll_queue) 239 | poller.poll_queue.clear() 240 | 241 | return infos, timed_out 242 | 243 | def cancelPoll(self, poller_uid): 244 | thr = self.m_owner.getThread() 245 | if not thr: 246 | return 247 | 248 | poller = thr.m_pollers.get(poller_uid) 249 | if not poller: 250 | return 251 | 252 | with poller.poll_cond: 253 | poller.cancelling = True 254 | poller.poll_cond.notify() 255 | 256 | # doStart in ntcore 257 | def start(self, *args): 258 | if not self.m_owner: 259 | self.m_owner = self.THREAD_CLASS(*args) 260 | self.m_owner.start() 261 | 262 | # Unlike ntcore, only a single argument is supported here. This is 263 | # to ensure that it's more clear what struct is being passed through 264 | 265 | def doAdd(self, item): 266 | self.start() 267 | thr = self.m_owner 268 | return thr.m_listeners.add(item) 269 | 270 | def send(self, only_listener, item): 271 | thr = self.m_owner 272 | if not thr or not thr.m_listeners: 273 | return 274 | 275 | thr.m_queue.put((only_listener, item)) 276 | -------------------------------------------------------------------------------- /tests/test_entry_notifier.py: -------------------------------------------------------------------------------- 1 | # ---------------------------------------------------------------------------- 2 | # Copyright (c) FIRST 2017. All Rights Reserved. 3 | # Open Source Software - may be modified and shared by FRC teams. The code 4 | # must be accompanied by the FIRST BSD license file in the root directory of 5 | # the project. 6 | # ---------------------------------------------------------------------------- 7 | 8 | # 9 | # These tests are adapted from ntcore's test suite 10 | # 11 | 12 | import pytest 13 | 14 | from _pynetworktables._impl.constants import ( 15 | NT_NOTIFY_IMMEDIATE, 16 | NT_NOTIFY_LOCAL, 17 | NT_NOTIFY_NEW, 18 | NT_NOTIFY_DELETE, 19 | NT_NOTIFY_UPDATE, 20 | NT_NOTIFY_FLAGS, 21 | ) 22 | 23 | from _pynetworktables._impl.entry_notifier import EntryNotifier 24 | from _pynetworktables._impl.value import Value 25 | 26 | 27 | @pytest.fixture 28 | def notifier(): 29 | n = EntryNotifier(verbose=True) 30 | n.start() 31 | yield n 32 | n.stop() 33 | 34 | 35 | def generateNotifications(notifier): 36 | # All flags combos that can be generated by Storage 37 | flags = [ 38 | # "normal" notifications 39 | NT_NOTIFY_NEW, 40 | NT_NOTIFY_DELETE, 41 | NT_NOTIFY_UPDATE, 42 | NT_NOTIFY_FLAGS, 43 | NT_NOTIFY_UPDATE | NT_NOTIFY_FLAGS, 44 | # immediate notifications are always "new" 45 | NT_NOTIFY_IMMEDIATE | NT_NOTIFY_NEW, 46 | # local notifications can be of any flag combo 47 | NT_NOTIFY_LOCAL | NT_NOTIFY_NEW, 48 | NT_NOTIFY_LOCAL | NT_NOTIFY_DELETE, 49 | NT_NOTIFY_LOCAL | NT_NOTIFY_UPDATE, 50 | NT_NOTIFY_LOCAL | NT_NOTIFY_FLAGS, 51 | NT_NOTIFY_LOCAL | NT_NOTIFY_UPDATE | NT_NOTIFY_FLAGS, 52 | ] 53 | 54 | # Generate across keys 55 | keys = ["/foo/bar", "/baz", "/boo"] 56 | 57 | val = Value.makeDouble(1) 58 | 59 | # Provide unique key indexes for each key 60 | keyindex = 5 61 | for key in keys: 62 | for flag in flags: 63 | notifier.notifyEntry(keyindex, key, val, flag) 64 | 65 | keyindex += 1 66 | 67 | 68 | def test_PollEntryMultiple(notifier): 69 | poller1 = notifier.createPoller() 70 | poller2 = notifier.createPoller() 71 | poller3 = notifier.createPoller() 72 | h1 = notifier.addPolledById(poller1, 6, NT_NOTIFY_NEW) 73 | h2 = notifier.addPolledById(poller2, 6, NT_NOTIFY_NEW) 74 | h3 = notifier.addPolledById(poller3, 6, NT_NOTIFY_UPDATE) 75 | 76 | assert not notifier.m_local_notifiers 77 | 78 | generateNotifications(notifier) 79 | 80 | assert notifier.waitForQueue(1.0) 81 | 82 | results1, timed_out = notifier.poll(poller1, 0) 83 | assert not timed_out 84 | results2, timed_out = notifier.poll(poller2, 0) 85 | assert not timed_out 86 | results3, timed_out = notifier.poll(poller3, 0) 87 | assert not timed_out 88 | 89 | assert len(results1) == 2 90 | for h, result in results1: 91 | print(result) 92 | assert h == h1 93 | 94 | assert len(results2) == 2 95 | for h, result in results2: 96 | print(result) 97 | assert h == h2 98 | 99 | assert len(results3) == 2 100 | for h, result in results3: 101 | print(result) 102 | assert h == h3 103 | 104 | 105 | def test_PollEntryBasic(notifier): 106 | poller = notifier.createPoller() 107 | g1 = notifier.addPolledById(poller, 6, NT_NOTIFY_NEW) 108 | g2 = notifier.addPolledById(poller, 6, NT_NOTIFY_DELETE) 109 | g3 = notifier.addPolledById(poller, 6, NT_NOTIFY_UPDATE) 110 | g4 = notifier.addPolledById(poller, 6, NT_NOTIFY_FLAGS) 111 | 112 | assert not notifier.m_local_notifiers 113 | 114 | generateNotifications(notifier) 115 | 116 | assert notifier.waitForQueue(1.0) 117 | 118 | timed_out = False 119 | results, timed_out = notifier.poll(poller, 0) 120 | assert not timed_out 121 | 122 | g1count = 0 123 | g2count = 0 124 | g3count = 0 125 | g4count = 0 126 | for h, result in results: 127 | print(h, result) 128 | assert result.name == "/baz" 129 | assert result.value == Value.makeDouble(1) 130 | assert result.local_id == 6 131 | 132 | if h == g1: 133 | g1count += 1 134 | assert (result.flags & NT_NOTIFY_NEW) != 0 135 | elif h == g2: 136 | g2count += 1 137 | assert (result.flags & NT_NOTIFY_DELETE) != 0 138 | elif h == g3: 139 | g3count += 1 140 | assert (result.flags & NT_NOTIFY_UPDATE) != 0 141 | elif h == g4: 142 | g4count += 1 143 | assert (result.flags & NT_NOTIFY_FLAGS) != 0 144 | else: 145 | assert False, "unknown listener index" 146 | 147 | assert g1count == 2 148 | assert g2count == 1 # NT_NOTIFY_DELETE 149 | assert g3count == 2 150 | assert g4count == 2 151 | 152 | 153 | def test_PollEntryImmediate(notifier): 154 | poller = notifier.createPoller() 155 | notifier.addPolledById(poller, 6, NT_NOTIFY_NEW | NT_NOTIFY_IMMEDIATE) 156 | notifier.addPolledById(poller, 6, NT_NOTIFY_NEW) 157 | 158 | assert not notifier.m_local_notifiers 159 | 160 | generateNotifications(notifier) 161 | 162 | assert notifier.waitForQueue(1.0) 163 | 164 | results, timed_out = notifier.poll(poller, 0) 165 | assert not timed_out 166 | print(results) 167 | assert len(results) == 4 168 | 169 | 170 | def test_PollEntryLocal(notifier): 171 | poller = notifier.createPoller() 172 | notifier.addPolledById(poller, 6, NT_NOTIFY_NEW | NT_NOTIFY_LOCAL) 173 | notifier.addPolledById(poller, 6, NT_NOTIFY_NEW) 174 | 175 | assert notifier.m_local_notifiers 176 | 177 | generateNotifications(notifier) 178 | 179 | assert notifier.waitForQueue(1.0) 180 | 181 | results, timed_out = notifier.poll(poller, 0) 182 | assert not timed_out 183 | print(results) 184 | assert len(results) == 6 185 | 186 | 187 | def test_PollPrefixMultiple(notifier): 188 | poller1 = notifier.createPoller() 189 | poller2 = notifier.createPoller() 190 | poller3 = notifier.createPoller() 191 | h1 = notifier.addPolled(poller1, "/foo", NT_NOTIFY_NEW) 192 | h2 = notifier.addPolled(poller2, "/foo", NT_NOTIFY_NEW) 193 | h3 = notifier.addPolled(poller3, "/foo", NT_NOTIFY_UPDATE) 194 | 195 | assert not notifier.m_local_notifiers 196 | 197 | generateNotifications(notifier) 198 | 199 | assert notifier.waitForQueue(1.0) 200 | 201 | results1, timed_out = notifier.poll(poller1, 0) 202 | assert not timed_out 203 | results2, timed_out = notifier.poll(poller2, 0) 204 | assert not timed_out 205 | results3, timed_out = notifier.poll(poller3, 0) 206 | assert not timed_out 207 | 208 | assert len(results1) == 2 209 | for h, result in results1: 210 | print(result) 211 | assert h == h1 212 | 213 | assert len(results2) == 2 214 | for h, result in results2: 215 | print(result) 216 | assert h == h2 217 | 218 | assert len(results3) == 2 219 | for h, result in results3: 220 | print(result) 221 | assert h == h3 222 | 223 | 224 | def test_PollPrefixBasic(notifier): 225 | poller = notifier.createPoller() 226 | g1 = notifier.addPolled(poller, "/foo", NT_NOTIFY_NEW) 227 | g2 = notifier.addPolled(poller, "/foo", NT_NOTIFY_DELETE) 228 | g3 = notifier.addPolled(poller, "/foo", NT_NOTIFY_UPDATE) 229 | g4 = notifier.addPolled(poller, "/foo", NT_NOTIFY_FLAGS) 230 | notifier.addPolled(poller, "/bar", NT_NOTIFY_NEW) 231 | notifier.addPolled(poller, "/bar", NT_NOTIFY_DELETE) 232 | notifier.addPolled(poller, "/bar", NT_NOTIFY_UPDATE) 233 | notifier.addPolled(poller, "/bar", NT_NOTIFY_FLAGS) 234 | 235 | assert not notifier.m_local_notifiers 236 | 237 | generateNotifications(notifier) 238 | 239 | assert notifier.waitForQueue(1.0) 240 | 241 | results, timed_out = notifier.poll(poller, 0) 242 | assert not timed_out 243 | 244 | g1count = 0 245 | g2count = 0 246 | g3count = 0 247 | g4count = 0 248 | for h, result in results: 249 | print(result) 250 | assert result.name.startswith("/foo") 251 | assert result.value == Value.makeDouble(1) 252 | 253 | if h == g1: 254 | g1count += 1 255 | assert (result.flags & NT_NOTIFY_NEW) != 0 256 | elif h == g2: 257 | g2count += 1 258 | assert (result.flags & NT_NOTIFY_DELETE) != 0 259 | elif h == g3: 260 | g3count += 1 261 | assert (result.flags & NT_NOTIFY_UPDATE) != 0 262 | elif h == g4: 263 | g4count += 1 264 | assert (result.flags & NT_NOTIFY_FLAGS) != 0 265 | else: 266 | assert False, "unknown listener index" 267 | 268 | assert g1count == 2 269 | assert g2count == 1 # NT_NOTIFY_DELETE 270 | assert g3count == 2 271 | assert g4count == 2 272 | 273 | 274 | def test_PollPrefixImmediate(notifier): 275 | poller = notifier.createPoller() 276 | notifier.addPolled(poller, "/foo", NT_NOTIFY_NEW | NT_NOTIFY_IMMEDIATE) 277 | notifier.addPolled(poller, "/foo", NT_NOTIFY_NEW) 278 | 279 | assert not notifier.m_local_notifiers 280 | 281 | generateNotifications(notifier) 282 | 283 | assert notifier.waitForQueue(1.0) 284 | 285 | results, timed_out = notifier.poll(poller, 0) 286 | assert not timed_out 287 | print(results) 288 | assert len(results) == 4 289 | 290 | 291 | def test_PollPrefixLocal(notifier): 292 | poller = notifier.createPoller() 293 | notifier.addPolled(poller, "/foo", NT_NOTIFY_NEW | NT_NOTIFY_LOCAL) 294 | notifier.addPolled(poller, "/foo", NT_NOTIFY_NEW) 295 | 296 | assert notifier.m_local_notifiers 297 | 298 | generateNotifications(notifier) 299 | 300 | assert notifier.waitForQueue(1.0) 301 | 302 | results, timed_out = notifier.poll(poller, 0) 303 | assert not timed_out 304 | print(results) 305 | assert len(results) == 6 306 | --------------------------------------------------------------------------------