diff --git a/README.md b/README.md index 5ebc9e1..9a5ac38 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,12 @@ git lfs pull uv sync ``` +## Run teleop demo +Use **WASD** for linear motion and **QE** for turning. **R** reloads the scene and **F** jumps to the next one. +```sh +uv run dataflow --teleop +``` + ## Testing with Artefacts @@ -59,6 +65,7 @@ The `dora-rs` nodes are organized as separate Python packages under `nodes/*` - [`tester`](./nodes/tester/) contains the test nodes that should be executed with `pytest` - [test_waypoints_poses.py](./nodes/tester/tester/test_waypoints_poses.py) Executes multiple waypoint navigation scenarios and uses the robot and waypoint position data to determine if the waypoint mission was successful. - [test_waypoints_report.py](./nodes/tester/tester/test_waypoints_report.py) The simplified version of the test above, that uses the internal waypoint mission state from the simulation to determine if the waypoint mission was successful. + - [`teleop`](./nodes/teleop/) implements keyboard teleop control ### Other packages - [`msgs`](./msgs/) Implements the necessary messages as python classes using [`arrow-message`](https://github.com/hennzau/arrow-message) diff --git a/dataflow/pyproject.toml b/dataflow/pyproject.toml index 6365a3e..aec6493 100644 --- a/dataflow/pyproject.toml +++ b/dataflow/pyproject.toml @@ -5,8 +5,12 @@ description = "Add your description here" readme = "README.md" requires-python = ">=3.11,<3.12" dependencies = [ - # "artefacts-cli>=0.9.9", - # "artefacts-click>=0.1.0", + "msgs", + "simulation", + "navigator", + "policy_controller", + "tester", + "teleop", "dora-rs-cli>=0.3.13", "typer>=0.20.0", ] diff --git a/dataflow/src/dataflow/dataflow.py b/dataflow/src/dataflow/dataflow.py index 9bf9b8e..843b5c2 100644 --- a/dataflow/src/dataflow/dataflow.py +++ b/dataflow/src/dataflow/dataflow.py @@ -4,15 +4,59 @@ from typing_extensions import Annotated from pathlib import Path +workspace_path = Path(__file__).parent.parent.parent.parent +output_path = workspace_path / "outputs/artefacts" +nodes_path = workspace_path / "nodes" +temp_dataflow_path = output_path / "dataflow.yaml" -def exec_dataflow(dataflow: DataflowBuilder, temp_dataflow_path: Path): + +def _exec_dataflow(dataflow: DataflowBuilder, temp_dataflow_path: Path): """Build and/or run the dataflow with dora-rs.""" dataflow.to_yaml(temp_dataflow_path) dora.build(str(temp_dataflow_path), uv=True) dora.run(str(temp_dataflow_path)) +def _create_base_dataflow() -> DataflowBuilder: + dataflow = DataflowBuilder(name="go2-example-dataflow") + + output_path.mkdir(parents=True, exist_ok=True) + + # Set up simulation + simulation = dataflow.add_node( + id="simulation", + path="simulation", + args="--scene generated_pyramid --use-auto-pilot", + env={ + "OMNI_KIT_ACCEPT_EULA": "YES", + "OUTPUT_DIR": str(output_path), + }, + ) + simulation.add_input("pub_status_tick", "dora/timer/millis/200") + simulation.add_input("joint_commands", "policy_controller/joint_commands") + simulation.add_output("robot_pose") + simulation.add_output("waypoints") + simulation.add_output("scene_info") + simulation.add_output("rtf") + simulation.add_output("observations") + simulation.add_output("simulation_time") + + # Set up policy controller + policy_controller = dataflow.add_node( + id="policy_controller", + path="policy_controller", + ) + policy_controller.add_input("observations", "simulation/observations") + policy_controller.add_input("clock", "simulation/simulation_time") + policy_controller.add_output("joint_commands") + + return dataflow, simulation, policy_controller + + def run_dataflow( + teleop: Annotated[ + bool, typer.Option(help="Use keyboard teleoperation to control the robot") + ] = False, test_waypoint_poses: Annotated[ bool, typer.Option(help="Run the waypoint poses tests") ] = False, @@ -23,6 +67,11 @@ def run_dataflow( ): """Compose the dataflow, and build/run it with dora-rs.""" + # We either test or teleop for now + if teleop and (test_waypoint_poses or test_waypoint_report or test_all): + print("Cannot use teleop and testing options at the same time.") + return + # List tests for running tests = [] if test_waypoint_poses or test_all: @@ -34,42 +83,24 @@ def run_dataflow( print("No tests selected to run. Use --help for options.") # TODO: run default dataflow without tester node (and optionally with teleop) - for test in tests: - dataflow = DataflowBuilder(name="go2-example-dataflow") - workspace_path = Path(__file__).parent.parent.parent.parent - output_path = workspace_path / "outputs/artefacts" - nodes_path = workspace_path / "nodes" - temp_dataflow_path = output_path / "dataflow.yaml" - - output_path.mkdir(parents=True, exist_ok=True) - - # Set up simulation - simulation = dataflow.add_node( - id="simulation", - path="simulation", - args="--scene generated_pyramid --use-auto-pilot", - env={ - "OMNI_KIT_ACCEPT_EULA": "YES", - "OUTPUT_DIR": str(output_path), - }, - ) - simulation.add_input("pub_status_tick", "dora/timer/millis/200") - simulation.add_input("joint_commands", "policy_controller/joint_commands") - simulation.add_output("robot_pose") - simulation.add_output("waypoints") - simulation.add_output("scene_info") - simulation.add_output("rtf") - simulation.add_output("observations") - simulation.add_output("simulation_time") - - policy_controller = dataflow.add_node( - id="policy_controller", - path="policy_controller", + if teleop: + dataflow, simulation, policy_controller = _create_base_dataflow() + + teleop_node = dataflow.add_node( + id="teleop", + path="teleop", ) - policy_controller.add_input("observations", "simulation/observations") - policy_controller.add_input("clock", "simulation/simulation_time") - policy_controller.add_input("command_2d", "navigator/command_2d") - policy_controller.add_output("joint_commands") + teleop_node.add_input("tick", "dora/timer/millis/100") + teleop_node.add_output("command_2d") + teleop_node.add_output("load_scene") + + policy_controller.add_input("command_2d", "teleop/command_2d") + simulation.add_input("load_scene", "teleop/load_scene") + + _exec_dataflow(dataflow, temp_dataflow_path) + + for test in tests: + dataflow, simulation, policy_controller = _create_base_dataflow() # Add waypoint navigation navigator = dataflow.add_node( @@ -80,6 +111,7 @@ def run_dataflow( navigator.add_input("robot_pose", "simulation/robot_pose") navigator.add_input("waypoints", "simulation/waypoints") navigator.add_output("command_2d") + policy_controller.add_input("command_2d", "navigator/command_2d") # Add the tester node tester = dataflow.add_node( @@ -102,7 +134,7 @@ def run_dataflow( # Allow the tester to load scenes in the simulation simulation.add_input("load_scene", "tester/load_scene") - exec_dataflow(dataflow, temp_dataflow_path) + _exec_dataflow(dataflow, temp_dataflow_path) def main(): diff --git a/nodes/navigator/navigator/main.py b/nodes/navigator/navigator/main.py index c2821be..6b934df 100644 --- a/nodes/navigator/navigator/main.py +++ b/nodes/navigator/navigator/main.py @@ -1,4 +1,4 @@ -"""TODO: Add docstring.""" +"""Simple navigator node that steers towards waypoints.""" import pyarrow as pa import numpy as np @@ -8,7 +8,7 @@ def main(): - """TODO: Add docstring.""" + """Publish Twist2D commands to navigate to waypoints.""" node = Node() print("Navigator node started.") diff --git a/nodes/teleop/README.md b/nodes/teleop/README.md new file mode 100644 index 0000000..5e74edf --- /dev/null +++ b/nodes/teleop/README.md @@ -0,0 +1,40 @@ +# teleop + +## Getting started + +- Install it with uv: + +```bash +uv venv -p 3.11 --seed +uv pip install -e . +``` + +## Contribution Guide + +- Format with [ruff](https://docs.astral.sh/ruff/): + +```bash +uv pip install ruff +uv run ruff check . --fix +``` + +- Lint with ruff: + +```bash +uv run ruff check . +``` + +- Test with [pytest](https://github.com/pytest-dev/pytest) + +```bash +uv pip install pytest +uv run pytest . # Test +``` + +## YAML Specification + +## Examples + +## License + +teleop's code are released under the MIT License diff --git a/nodes/teleop/pyproject.toml b/nodes/teleop/pyproject.toml new file mode 100644 index 0000000..c3097bd --- /dev/null +++ b/nodes/teleop/pyproject.toml @@ -0,0 +1,26 @@ +[project] +name = "teleop" +version = "0.0.0" +authors = [{ name = "Your Name", email = "email@email.com" }] +description = "teleop" +license = { text = "MIT" } +readme = "README.md" +requires-python = ">=3.8" + +dependencies = [ + "msgs", + "dora-rs >= 0.3.9", + "pynput>=1.8.1", +] + +[dependency-groups] +dev = ["pytest >=8.1.1", "ruff >=0.9.1"] + +[project.scripts] +teleop = "teleop.main:main" + +[tool.ruff.lint] +extend-select = [ + "D", # pydocstyle + "UP" +] diff --git a/nodes/teleop/teleop/__init__.py b/nodes/teleop/teleop/__init__.py new file mode 100644 index 0000000..885869a --- /dev/null +++ b/nodes/teleop/teleop/__init__.py @@ -0,0 +1,13 @@ +"""Outputs a 2D twist command based on keyboard teleoperation.""" + +import os + +# Define the path to the README file relative to the package directory +readme_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "README.md") + +# Read the content of the README file +try: + with open(readme_path, encoding="utf-8") as f: + __doc__ = f.read() +except FileNotFoundError: + __doc__ = "README file not found." diff --git a/nodes/teleop/teleop/__main__.py b/nodes/teleop/teleop/__main__.py new file mode 100644 index 0000000..bbc8696 --- /dev/null +++ b/nodes/teleop/teleop/__main__.py @@ -0,0 +1,6 @@ +# # noqa: D100 + +from .main import main + +if __name__ == "__main__": + main() diff --git a/nodes/teleop/teleop/main.py b/nodes/teleop/teleop/main.py new file mode 100644 index 0000000..5410875 --- /dev/null +++ b/nodes/teleop/teleop/main.py @@ -0,0 +1,106 @@ +"""Keyboard teleoperation node with holonomic control and scene selection.""" + +from __future__ import annotations + +from typing import Final + +from dora import Node +from pynput import keyboard + +import msgs + +LINEAR_SPEED: Final[float] = 1.2 # m/s +ANGULAR_SPEED: Final[float] = 1.2 # rad/s + +MOVEMENT_KEYS = { + keyboard.KeyCode.from_char("w"), + keyboard.KeyCode.from_char("a"), + keyboard.KeyCode.from_char("s"), + keyboard.KeyCode.from_char("d"), + keyboard.KeyCode.from_char("q"), + keyboard.KeyCode.from_char("e"), +} + +KEY_NEXT_SCENE = keyboard.KeyCode.from_char("f") +KEY_RELOAD_SCENE = keyboard.KeyCode.from_char("r") + +SCENES: Final[list[tuple[str, float]]] = [ + ("generated_pyramid", 0.7), + ("rail_blocks", 1.0), + ("stone_stairs", 1.0), + ("excavator", 1.0), +] + + +def get_twist(movement_pressed: set[keyboard.KeyCode | keyboard.Key]) -> msgs.Twist2D: + """Calculate twist based on pressed keys.""" + forward = int(keyboard.KeyCode.from_char("w") in movement_pressed) - int( + keyboard.KeyCode.from_char("s") in movement_pressed + ) + strafe = int(keyboard.KeyCode.from_char("a") in movement_pressed) - int( + keyboard.KeyCode.from_char("d") in movement_pressed + ) + rotate = int(keyboard.KeyCode.from_char("q") in movement_pressed) - int( + keyboard.KeyCode.from_char("e") in movement_pressed + ) + return msgs.Twist2D( + linear_x=forward * LINEAR_SPEED, + linear_y=strafe * LINEAR_SPEED, + angular_z=rotate * ANGULAR_SPEED, + ) + + +def main() -> None: + """Entrypoint wiring the teleop publisher.""" + node = Node() + + movement_pressed: set[keyboard.KeyCode | keyboard.Key] = set() + scene_index = 0 + + def publish_scene_info() -> None: + name, difficulty = SCENES[scene_index] + scene = msgs.SceneInfo(name=name, difficulty=difficulty) + node.send_output("load_scene", scene.to_arrow()) + print(f"[teleop] Loading scene '{name}' (difficulty {difficulty})") + + # Initial scene publish + publish_scene_info() + + try: + with keyboard.Events() as events: + while True: + # Check for dora events with a small timeout to keep the loop spinning + dora_event = node.next(timeout=0.01) + + if dora_event is not None: + if dora_event["type"] == "INPUT": + if dora_event["id"] == "tick": + twist = get_twist(movement_pressed) + node.send_output("command_2d", twist.to_arrow()) + elif dora_event["id"] == "stop": + break + + # Drain all pending keyboard events + while True: + key_event = events.get(0.0) + if key_event is None: + break + + if isinstance(key_event, keyboard.Events.Press): + key = key_event.key + if key in MOVEMENT_KEYS: + movement_pressed.add(key) + elif key == KEY_NEXT_SCENE: + scene_index = (scene_index + 1) % len(SCENES) + publish_scene_info() + elif key == KEY_RELOAD_SCENE: + publish_scene_info() + + elif isinstance(key_event, keyboard.Events.Release): + key = key_event.key + if key in MOVEMENT_KEYS: + movement_pressed.discard(key) + except KeyboardInterrupt: + pass + finally: + node.send_output("command_2d", msgs.Twist2D().to_arrow()) diff --git a/nodes/teleop/tests/test_teleop.py b/nodes/teleop/tests/test_teleop.py new file mode 100644 index 0000000..52a88eb --- /dev/null +++ b/nodes/teleop/tests/test_teleop.py @@ -0,0 +1,13 @@ +"""Test module for teleop package.""" + +import pytest + + +def test_import_main(): + """Test importing and running the main function.""" + from teleop.main import main + + # Check that everything is working, and catch Dora RuntimeError + # as we're not running in a Dora dataflow. + with pytest.raises(RuntimeError): + main() diff --git a/pyproject.toml b/pyproject.toml index 788cda7..8e6d4e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,10 +20,14 @@ navigator = { workspace = true } policy_controller = { workspace = true } tester = { workspace = true } dataflow = { workspace = true } +teleop = { workspace = true } [tool.uv.workspace] members = ["nodes/*", "msgs", "dataflow"] [dependency-groups] -dev = ["ruff >=0.14.3"] \ No newline at end of file +dev = [ + "ruff >=0.14.3", + "pre-commit", +] \ No newline at end of file diff --git a/uv.lock b/uv.lock index be366a3..1f18f57 100644 --- a/uv.lock +++ b/uv.lock @@ -16,6 +16,7 @@ members = [ "navigator", "policy-controller", "simulation", + "teleop", "tester", ] @@ -316,6 +317,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, ] +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, +] + [[package]] name = "charset-normalizer" version = "3.3.2" @@ -452,6 +462,12 @@ version = "0.1.0" source = { editable = "dataflow" } dependencies = [ { name = "dora-rs-cli" }, + { name = "msgs" }, + { name = "navigator" }, + { name = "policy-controller" }, + { name = "simulation" }, + { name = "teleop" }, + { name = "tester" }, { name = "typer" }, ] @@ -463,12 +479,27 @@ dev = [ [package.metadata] requires-dist = [ { name = "dora-rs-cli", specifier = ">=0.3.13" }, + { name = "msgs", editable = "msgs" }, + { name = "navigator", editable = "nodes/navigator" }, + { name = "policy-controller", editable = "nodes/policy_controller" }, + { name = "simulation", editable = "nodes/simulation" }, + { name = "teleop", editable = "nodes/teleop" }, + { name = "tester", editable = "nodes/tester" }, { name = "typer", specifier = ">=0.20.0" }, ] [package.metadata.requires-dev] dev = [{ name = "ruff", specifier = ">=0.14.3" }] +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + [[package]] name = "dora-rs" version = "0.3.13" @@ -513,6 +544,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/eb/29/005e12a00e4475c30185f4a3998fa82370eba689a8133227ef591924ee37/dora_rs_cli-0.3.13-cp37-abi3-win_amd64.whl", hash = "sha256:022b031f1416c4ae0155e61f832f7fddfb822c1a50b223e3b2bb4a96fe915f84", size = 14110816, upload-time = "2025-10-01T16:31:25.454Z" }, ] +[[package]] +name = "evdev" +version = "1.9.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/fe/a17c106a1f4061ce83f04d14bcedcfb2c38c7793ea56bfb906a6fadae8cb/evdev-1.9.2.tar.gz", hash = "sha256:5d3278892ce1f92a74d6bf888cc8525d9f68af85dbe336c95d1c87fb8f423069", size = 33301, upload-time = "2025-05-01T19:53:47.69Z" } + [[package]] name = "fastapi" version = "0.115.7" @@ -603,6 +640,7 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "pre-commit" }, { name = "ruff" }, ] @@ -619,7 +657,10 @@ requires-dist = [ ] [package.metadata.requires-dev] -dev = [{ name = "ruff", specifier = ">=0.14.3" }] +dev = [ + { name = "pre-commit" }, + { name = "ruff", specifier = ">=0.14.3" }, +] [[package]] name = "gunicorn" @@ -657,6 +698,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/14/e4/20d28dfe7f5b5603b6b04c33bb88662ad749de51f0c539a561f235f42666/httptools-0.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:5cceac09f164bcba55c0500a18fe3c47df29b62353198e4f37bbcc5d591172c3", size = 55434, upload-time = "2023-10-16T17:42:01.414Z" }, ] +[[package]] +name = "identify" +version = "2.6.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311, upload-time = "2025-10-02T17:43:40.631Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" }, +] + [[package]] name = "idna" version = "3.10" @@ -1290,6 +1340,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/38/e9/5f72929373e1a0e8d142a130f3f97e6ff920070f87f91c4e13e40e0fba5a/networkx-3.3-py3-none-any.whl", hash = "sha256:28575580c6ebdaf4505b22c6256a2b9de86b316dc63ba9e93abde3d78dfdbcf2", size = 1702396, upload-time = "2024-04-06T12:59:44.283Z" }, ] +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, +] + [[package]] name = "numba" version = "0.59.1" @@ -1584,6 +1643,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/db/1b/97d8fd443d89e8d39b3aaa95aa70a184f752b68d2cc803f7fedab8dfd81f/Pint-0.20.1-py3-none-any.whl", hash = "sha256:68afe65665542ee3ec99f69f043b1d39bfe7c6d61b786940157138fd08b838fb", size = 269457, upload-time = "2022-10-27T13:32:58.548Z" }, ] +[[package]] +name = "platformdirs" +version = "4.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" }, +] + [[package]] name = "plotly" version = "5.3.1" @@ -1658,6 +1726,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/16/87/50ebb4d593b6fc9e294901f4feed766f6694eb6cbbca06e93596c3397e8e/posetree-1.1.2-py3-none-any.whl", hash = "sha256:65bc306c13a345ac91dd4702cce9bfe8ede0acc497c20313dab106ab6263b0b6", size = 13498, upload-time = "2024-12-04T23:49:50.597Z" }, ] +[[package]] +name = "pre-commit" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/49/7845c2d7bf6474efd8e27905b51b11e6ce411708c91e829b93f324de9929/pre_commit-4.4.0.tar.gz", hash = "sha256:f0233ebab440e9f17cabbb558706eb173d19ace965c68cdce2c081042b4fab15", size = 197501, upload-time = "2025-11-08T21:12:11.607Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/11/574fe7d13acf30bfd0a8dd7fa1647040f2b8064f13f43e8c963b1e65093b/pre_commit-4.4.0-py2.py3-none-any.whl", hash = "sha256:b35ea52957cbf83dcc5d8ee636cbead8624e3a15fbfa61a370e42158ac8a5813", size = 226049, upload-time = "2025-11-08T21:12:10.228Z" }, +] + [[package]] name = "propcache" version = "0.2.1" @@ -1823,6 +1907,85 @@ crypto = [ { name = "cryptography" }, ] +[[package]] +name = "pynput" +version = "1.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "evdev", marker = "'linux' in sys_platform" }, + { name = "pyobjc-framework-applicationservices", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, + { name = "python-xlib", marker = "'linux' in sys_platform" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/c3/dccf44c68225046df5324db0cc7d563a560635355b3e5f1d249468268a6f/pynput-1.8.1.tar.gz", hash = "sha256:70d7c8373ee98911004a7c938742242840a5628c004573d84ba849d4601df81e", size = 82289, upload-time = "2025-03-17T17:12:01.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/4f/ac3fa906ae8a375a536b12794128c5efacade9eaa917a35dfd27ce0c7400/pynput-1.8.1-py2.py3-none-any.whl", hash = "sha256:42dfcf27404459ca16ca889c8fb8ffe42a9fe54f722fd1a3e130728e59e768d2", size = 91693, upload-time = "2025-03-17T17:12:00.094Z" }, +] + +[[package]] +name = "pyobjc-core" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/b6/d5612eb40be4fd5ef88c259339e6313f46ba67577a95d86c3470b951fce0/pyobjc_core-12.1.tar.gz", hash = "sha256:2bb3903f5387f72422145e1466b3ac3f7f0ef2e9960afa9bcd8961c5cbf8bd21", size = 1000532, upload-time = "2025-11-14T10:08:28.292Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/df/d2b290708e9da86d6e7a9a2a2022b91915cf2e712a5a82e306cb6ee99792/pyobjc_core-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c918ebca280925e7fcb14c5c43ce12dcb9574a33cccb889be7c8c17f3bcce8b6", size = 671263, upload-time = "2025-11-14T09:31:35.231Z" }, +] + +[[package]] +name = "pyobjc-framework-applicationservices" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coretext", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/6a/d4e613c8e926a5744fc47a9e9fea08384a510dc4f27d844f7ad7a2d793bd/pyobjc_framework_applicationservices-12.1.tar.gz", hash = "sha256:c06abb74f119bc27aeb41bf1aef8102c0ae1288aec1ac8665ea186a067a8945b", size = 103247, upload-time = "2025-11-14T10:08:52.18Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/86/d07eff705ff909a0ffa96d14fc14026e9fc9dd716233648c53dfd5056b8e/pyobjc_framework_applicationservices-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bdddd492eeac6d14ff2f5bd342aba29e30dffa72a2d358c08444da22129890e2", size = 32784, upload-time = "2025-11-14T09:36:08.755Z" }, +] + +[[package]] +name = "pyobjc-framework-cocoa" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/a3/16ca9a15e77c061a9250afbae2eae26f2e1579eb8ca9462ae2d2c71e1169/pyobjc_framework_cocoa-12.1.tar.gz", hash = "sha256:5556c87db95711b985d5efdaaf01c917ddd41d148b1e52a0c66b1a2e2c5c1640", size = 2772191, upload-time = "2025-11-14T10:13:02.069Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/07/5760735c0fffc65107e648eaf7e0991f46da442ac4493501be5380e6d9d4/pyobjc_framework_cocoa-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f52228bcf38da64b77328787967d464e28b981492b33a7675585141e1b0a01e6", size = 383812, upload-time = "2025-11-14T09:40:53.169Z" }, +] + +[[package]] +name = "pyobjc-framework-coretext" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/da/682c9c92a39f713bd3c56e7375fa8f1b10ad558ecb075258ab6f1cdd4a6d/pyobjc_framework_coretext-12.1.tar.gz", hash = "sha256:e0adb717738fae395dc645c9e8a10bb5f6a4277e73cba8fa2a57f3b518e71da5", size = 90124, upload-time = "2025-11-14T10:14:38.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/81/7b8efc41e743adfa2d74b92dec263c91bcebfb188d2a8f5eea1886a195ff/pyobjc_framework_coretext-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4f6742ba5b0bb7629c345e99eff928fbfd9e9d3d667421ac1a2a43bdb7ba9833", size = 29990, upload-time = "2025-11-14T09:47:01.206Z" }, +] + +[[package]] +name = "pyobjc-framework-quartz" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/18/cc59f3d4355c9456fc945eae7fe8797003c4da99212dd531ad1b0de8a0c6/pyobjc_framework_quartz-12.1.tar.gz", hash = "sha256:27f782f3513ac88ec9b6c82d9767eef95a5cf4175ce88a1e5a65875fee799608", size = 3159099, upload-time = "2025-11-14T10:21:24.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ef/dcd22b743e38b3c430fce4788176c2c5afa8bfb01085b8143b02d1e75201/pyobjc_framework_quartz-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:19f99ac49a0b15dd892e155644fe80242d741411a9ed9c119b18b7466048625a", size = 217795, upload-time = "2025-11-14T09:59:46.922Z" }, +] + [[package]] name = "pyparsing" version = "3.0.9" @@ -1905,6 +2068,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, ] +[[package]] +name = "python-xlib" +version = "0.33" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/f5/8c0653e5bb54e0cbdfe27bf32d41f27bc4e12faa8742778c17f2a71be2c0/python-xlib-0.33.tar.gz", hash = "sha256:55af7906a2c75ce6cb280a584776080602444f75815a7aff4d287bb2d7018b32", size = 269068, upload-time = "2022-12-25T18:53:00.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/b8/ff33610932e0ee81ae7f1269c890f697d56ff74b9f5b2ee5d9b7fa2c5355/python_xlib-0.33-py2.py3-none-any.whl", hash = "sha256:c3534038d42e0df2f1392a1b30a15a4ff5fdc2b86cfa94f072bf11b10a164398", size = 182185, upload-time = "2022-12-25T18:52:58.662Z" }, +] + [[package]] name = "pytz" version = "2024.1" @@ -2204,6 +2379,35 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/99/ff/c87e0622b1dadea79d2fb0b25ade9ed98954c9033722eb707053d310d4f3/sympy-1.13.3-py3-none-any.whl", hash = "sha256:54612cf55a62755ee71824ce692986f23c88ffa77207b30c1368eda4a7060f73", size = 6189483, upload-time = "2024-09-18T21:54:23.097Z" }, ] +[[package]] +name = "teleop" +version = "0.0.0" +source = { editable = "nodes/teleop" } +dependencies = [ + { name = "dora-rs" }, + { name = "msgs" }, + { name = "pynput" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "dora-rs", specifier = ">=0.3.9" }, + { name = "msgs", editable = "msgs" }, + { name = "pynput", specifier = ">=1.8.1" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=8.1.1" }, + { name = "ruff", specifier = ">=0.9.1" }, +] + [[package]] name = "tenacity" version = "9.1.2" @@ -2487,6 +2691,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/73/f5/cbb16fcbe277c1e0b8b3ddd188f2df0e0947f545c49119b589643632d156/uvicorn-0.29.0-py3-none-any.whl", hash = "sha256:2c2aac7ff4f4365c206fd773a39bf4ebd1047c238f8b8268ad996829323473de", size = 60813, upload-time = "2024-03-20T06:43:21.841Z" }, ] +[[package]] +name = "virtualenv" +version = "20.35.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846bd9df527390ecc26b3805a0c5989048c210e22c5ca9/virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c", size = 6028799, upload-time = "2025-10-29T06:57:40.511Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" }, +] + [[package]] name = "watchdog" version = "4.0.0"