pyforce-rosie 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. pyforce_rosie-2.0.0.dist-info/METADATA +166 -0
  2. pyforce_rosie-2.0.0.dist-info/RECORD +72 -0
  3. pyforce_rosie-2.0.0.dist-info/WHEEL +4 -0
  4. pyforce_rosie-2.0.0.dist-info/entry_points.txt +2 -0
  5. pyforce_rosie-2.0.0.dist-info/licenses/LICENSE +21 -0
  6. rosie/__init__.py +1 -0
  7. rosie/__main__.py +17 -0
  8. rosie/apis/__init__.py +0 -0
  9. rosie/apis/models.py +11 -0
  10. rosie/apis/web/__init__.py +0 -0
  11. rosie/apis/web/models.py +57 -0
  12. rosie/apis/web/routes.py +228 -0
  13. rosie/apis/web/server.py +59 -0
  14. rosie/builtin/maps/house.geojson +23 -0
  15. rosie/builtin/profiles/blubot/profile.toml +40 -0
  16. rosie/builtin/profiles/blubot/thumbnail.png +0 -0
  17. rosie/builtin/profiles/blubot/vector.svg +177 -0
  18. rosie/builtin/profiles/kevin/profile.toml +39 -0
  19. rosie/builtin/profiles/kevin/thumbnail.png +0 -0
  20. rosie/builtin/profiles/kevin/vector.svg +253 -0
  21. rosie/builtin/profiles/ltl/profile.toml +42 -0
  22. rosie/builtin/profiles/ltl/thumbnail.png +0 -0
  23. rosie/builtin/profiles/ltl/vector.svg +217 -0
  24. rosie/builtin/profiles/rovert/profile.toml +40 -0
  25. rosie/builtin/profiles/rovert/thumbnail.png +0 -0
  26. rosie/builtin/profiles/rovert/vector.svg +115 -0
  27. rosie/builtin/profiles/simubot/profile.toml +32 -0
  28. rosie/builtin/profiles/simubot/thumbnail.png +0 -0
  29. rosie/builtin/profiles/simubot/vector.svg +232 -0
  30. rosie/cli.py +96 -0
  31. rosie/client/__init__.py +16 -0
  32. rosie/client/api.py +183 -0
  33. rosie/client/models.py +22 -0
  34. rosie/control/__init__.py +0 -0
  35. rosie/control/models.py +72 -0
  36. rosie/control/movement.py +136 -0
  37. rosie/control/regulation.py +34 -0
  38. rosie/control/teleoperation.py +28 -0
  39. rosie/control/tracking.py +42 -0
  40. rosie/core.py +216 -0
  41. rosie/errors.py +19 -0
  42. rosie/hardware/__init__.py +0 -0
  43. rosie/hardware/buses.py +37 -0
  44. rosie/hardware/drive/__init__.py +19 -0
  45. rosie/hardware/drive/devices/__init__.py +0 -0
  46. rosie/hardware/drive/devices/arduino.py +96 -0
  47. rosie/hardware/drive/devices/md25.py +157 -0
  48. rosie/hardware/drive/devices/virtual.py +69 -0
  49. rosie/hardware/drive/kinematics.py +12 -0
  50. rosie/hardware/drive/models.py +63 -0
  51. rosie/hardware/drive/pid.py +22 -0
  52. rosie/hardware/models.py +19 -0
  53. rosie/hardware/sensors/__init__.py +19 -0
  54. rosie/hardware/sensors/models.py +12 -0
  55. rosie/hardware/sensors/usound.py +207 -0
  56. rosie/localization/__init__.py +0 -0
  57. rosie/localization/odometry.py +40 -0
  58. rosie/models.py +144 -0
  59. rosie/planning/__init__.py +0 -0
  60. rosie/planning/models.py +36 -0
  61. rosie/planning/path.py +25 -0
  62. rosie/planning/search.py +47 -0
  63. rosie/planning/trajectory.py +156 -0
  64. rosie/planning/visibility.py +68 -0
  65. rosie/profile.py +91 -0
  66. rosie/py.typed +0 -0
  67. rosie/recording.py +102 -0
  68. rosie/robot.py +51 -0
  69. rosie/service.py +86 -0
  70. rosie/world/__init__.py +0 -0
  71. rosie/world/maps.py +69 -0
  72. rosie/world/models.py +16 -0
@@ -0,0 +1,166 @@
1
+ Metadata-Version: 2.5
2
+ Name: pyforce-rosie
3
+ Version: 2.0.0
4
+ Summary: Mobile robot control in plain Python, for learning and teaching robotics
5
+ Project-URL: Repository, https://github.com/PyForce/rosie
6
+ Project-URL: Issues, https://github.com/PyForce/rosie/issues
7
+ Author: Rolando Urquiza, Fernando Martínez González, Gustavo Viera-López, Antonio Serrano Muñoz, Danilo Gómez Gómez, Alex Coto, Silvio Delgado, Jesús Garnica, Sergio Frayle Pérez
8
+ Maintainer-email: Gustavo Viera-López <gvieralopez@gmail.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: differential drive,mobile robots,path planning,robotics
12
+ Requires-Python: >=3.12
13
+ Requires-Dist: websockets>=15.0
14
+ Provides-Extra: robot
15
+ Requires-Dist: fastapi>=0.162; extra == 'robot'
16
+ Requires-Dist: numpy>=2.5; extra == 'robot'
17
+ Requires-Dist: pydantic>=2.14; extra == 'robot'
18
+ Requires-Dist: pyserial>=3.5; extra == 'robot'
19
+ Requires-Dist: shapely>=2.2; extra == 'robot'
20
+ Requires-Dist: smbus2>=0.6; extra == 'robot'
21
+ Requires-Dist: typer>=0.27; extra == 'robot'
22
+ Requires-Dist: uvicorn[standard]>=0.54; extra == 'robot'
23
+ Description-Content-Type: text/markdown
24
+
25
+ <div align="center">
26
+
27
+ <img src="https://raw.githubusercontent.com/PyForce/rosie/main/assets/logo.svg" alt="" width="96">
28
+
29
+ # rosie
30
+
31
+ **Learn mobile robotics by reading the robot**
32
+
33
+ [![QA](https://github.com/PyForce/rosie/actions/workflows/qa.yml/badge.svg)](https://github.com/PyForce/rosie/actions/workflows/qa.yml)
34
+ [![Release](https://img.shields.io/github/v/release/PyForce/rosie?label=release)](https://github.com/PyForce/rosie/releases/latest)
35
+ [![PyPI](https://img.shields.io/pypi/v/pyforce-rosie)](https://pypi.org/project/pyforce-rosie/)
36
+ [![Docker image](https://img.shields.io/badge/docker-ghcr.io-2496ED?logo=docker&logoColor=white)](https://github.com/PyForce/rosie/pkgs/container/rosie)
37
+ [![Docs](https://readthedocs.org/projects/pyforce-rosie/badge/?version=latest)](https://pyforce-rosie.readthedocs.io)
38
+ [![Python 3.12+](https://img.shields.io/badge/python-3.12%2B-3776AB?logo=python&logoColor=white)](https://www.python.org/)
39
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](https://github.com/PyForce/rosie/blob/main/LICENSE)
40
+ <br>
41
+ [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
42
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
43
+ [![Checked with mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/)
44
+
45
+ [Quick start](#quick-start) · [What you can learn](#what-you-can-learn) · [Documentation](#documentation) · [Development](#development)
46
+
47
+ </div>
48
+
49
+ ---
50
+
51
+ rosie is a complete robot stack (localization, planning, control, drives) for two-wheeled robots, written as
52
+ short Python modules you can read, change and run on a simulated robot or a real one. It estimates where the
53
+ robot is from its wheel encoders, plans routes around walls and furniture and trajectories along them, tracks
54
+ them with feedback controllers, and lets you drive by hand from a browser or from Python.
55
+
56
+ It's made for learning and teaching robotics, from the control equations to the web page that drives the
57
+ robot.
58
+
59
+ ![WebRosie, rosie's web page, with a robot in the built-in house](https://raw.githubusercontent.com/PyForce/rosie/main/docs/images/webrosie.png)
60
+
61
+ ## Quick start
62
+
63
+ rosie runs next to the robot, or next to a simulated one, and takes orders over a web API. There are
64
+ several ways to use it: from a browser, from Python, from plain HTTP, or by changing rosie itself. Each
65
+ one below starts with the simulated robot, so no hardware is needed.
66
+
67
+ ### In the browser
68
+
69
+ ```bash
70
+ docker run -p 8080:8080 ghcr.io/pyforce/rosie
71
+ ```
72
+
73
+ Open <http://localhost:8080>: [WebRosie](https://github.com/PyForce/WebRosie), with a simulated robot in a
74
+ house. Click the map to send it somewhere, or drive it with the keyboard.
75
+
76
+ ### As a Python package
77
+
78
+ ```bash
79
+ pip install "pyforce-rosie[robot]"
80
+ rosie start
81
+ ```
82
+
83
+ This runs rosie's web API on port 5000, without the web interface. Flags or `ROSIE_*` environment
84
+ variables pick another robot, map or port.
85
+
86
+ ### From a Python script or notebook
87
+
88
+ With rosie running (either way above), on the same computer or any other:
89
+
90
+ ```python
91
+ import time
92
+
93
+ from rosie.client import RosieClient # pip install pyforce-rosie
94
+
95
+ robot = RosieClient("http://localhost:8080") # 5000 with rosie start
96
+ print(robot.odometry()) # Position(x=0.0, y=0.0, theta=0.0)
97
+ robot.goto(1, 0.5, 0.3) # to (1 m, 0.5 m) at 0.3 m/s
98
+ time.sleep(5)
99
+ print(robot.odometry()) # about Position(x=1.0, y=0.5, theta=0.46)
100
+ ```
101
+
102
+ ### Over HTTP
103
+
104
+ The web API is plain HTTP and JSON, so any language or tool can drive the robot:
105
+
106
+ ```bash
107
+ curl -X POST localhost:8080/goto -d '{"target": [1, 0.5], "speed": 0.3}'
108
+ curl localhost:8080/odometry
109
+ ```
110
+
111
+ ### From the source
112
+
113
+ To change rosie itself, for example to try your own controller:
114
+
115
+ ```bash
116
+ git clone https://github.com/PyForce/rosie
117
+ cd rosie
118
+ uv sync
119
+ uv run rosie start
120
+ ```
121
+
122
+ [Getting started](https://github.com/PyForce/rosie/blob/main/docs/getting-started.md) covers each of these in more detail, plus configuration.
123
+
124
+ ## What you can learn
125
+
126
+ | Topic | In rosie |
127
+ |---|---|
128
+ | Wheel odometry | second-order Runge-Kutta, from encoder counts |
129
+ | Trajectory generation | straight lines, or cubic polynomials that round the corners |
130
+ | Trajectory tracking | input-output linearization; plug in your own controller as one class |
131
+ | Reaching a point | a polar go-to-point controller |
132
+ | Wheel speed control | an incremental PID |
133
+ | Route planning | free space, a visibility graph, A* |
134
+ | Hardware | serial and I2C motor drives, an ultrasound sensor board |
135
+ | Integration | a REST + WebSocket API, a web interface, a Python client |
136
+
137
+ The simulated robot runs on a clock you control, so a script can simulate a 10-second movement in
138
+ milliseconds, the same every time. Every planned movement can be recorded to CSV, one row per control step,
139
+ ready for pandas.
140
+
141
+ ## Documentation
142
+
143
+ - [Getting started](https://github.com/PyForce/rosie/blob/main/docs/getting-started.md): running, driving and configuring rosie.
144
+ - [How rosie works](https://github.com/PyForce/rosie/blob/main/docs/how-rosie-works.md): the control loop, and where everything lives.
145
+ - [Motion control](https://github.com/PyForce/rosie/blob/main/docs/motion-control.md) and [Navigation](https://github.com/PyForce/rosie/blob/main/docs/navigation.md): the algorithms, with their
146
+ equations.
147
+ - [Teaching with rosie](https://github.com/PyForce/rosie/blob/main/docs/teaching.md): lab ideas.
148
+ - [Robots and hardware](https://github.com/PyForce/rosie/blob/main/docs/robots.md), [Web API and extensions](https://github.com/PyForce/rosie/blob/main/docs/integration.md),
149
+ [Python client](https://github.com/PyForce/rosie/blob/main/docs/client.md).
150
+
151
+ ## Development
152
+
153
+ Setup, QA, docs and releases: [DEVELOPMENT.md](https://github.com/PyForce/rosie/blob/main/DEVELOPMENT.md).
154
+
155
+ ## Contributing
156
+
157
+ Issues and pull requests are welcome; `make qa` must pass.
158
+
159
+ ## License
160
+
161
+ MIT: see [LICENSE](https://github.com/PyForce/rosie/blob/main/LICENSE). rosie is a [PyForce](https://github.com/PyForce) project, started in 2015:
162
+ [authors](https://github.com/PyForce/rosie/blob/main/docs/about.md).
163
+
164
+ ---
165
+
166
+ <sub>🚀 Project starter provided by [Cookie Pyrate v1.1.0](https://github.com/gvieralopez/cookie-pyrate/tree/v1.1.0)</sub>
@@ -0,0 +1,72 @@
1
+ rosie/__init__.py,sha256=_7OlQdbVkK4jad0CLdpI0grT-zEAb-qgFmH5mFzDXiA,22
2
+ rosie/__main__.py,sha256=EI3XZvVxKxqafFFEZNVOdoDQPURBR0vJph03j7syrkY,401
3
+ rosie/cli.py,sha256=J3IIXoA9qIYBMboCNQEGCuWEjrwRPlfNcPR5SaiCP_M,3039
4
+ rosie/core.py,sha256=VaH5kd-Gb7zwrSzw4LcpTPKnr_2lx_IOm4YXWC-E1Wo,8217
5
+ rosie/errors.py,sha256=zeBtg8pMyHe-96qhdg5ZFc0eNPVNW0bQEOW1lld0lmI,640
6
+ rosie/models.py,sha256=0fSD9_aL3rmqJMXzci7YzOoemszGBBXtPU3xr_ISJUI,3834
7
+ rosie/profile.py,sha256=yKMONHhoez6EhCP8F1KlddKrpcf3PBCrQAQxk5h8BGA,3387
8
+ rosie/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ rosie/recording.py,sha256=j2Vy1vNS4JRlVXuPxJVhaI6Bt9S4zR6KcnwNKsv7hYw,3721
10
+ rosie/robot.py,sha256=iqLJR4YcjHcL6G8LumvHH7F27MXhmqUFSY0fYdezG0U,1899
11
+ rosie/service.py,sha256=eJE0U3pYSZV_5TswqLyOHxQMZHpVPrPTpUGdgZ-P8Ks,2936
12
+ rosie/apis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ rosie/apis/models.py,sha256=Iu0YNtSbLI6cNaw9xsLWml0n_6NT9Uy2uHczN8yWwgk,356
14
+ rosie/apis/web/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ rosie/apis/web/models.py,sha256=gqp1vqmguIQF-HV0P_oOsthpVfu_D6Fx-NEXytb95vA,1381
16
+ rosie/apis/web/routes.py,sha256=bNq7qjoh9bfmro6scuW7T4E46jGUUzXNf2X9Cib4S74,7337
17
+ rosie/apis/web/server.py,sha256=HNoVAyD4606xJ84MExPmpjgG2e57n9813GFJwhZcGXE,2099
18
+ rosie/builtin/maps/house.geojson,sha256=Fafj-ZsWetupjhppQrZjD3HdzMfMmUehbIbmIjdWOwU,3792
19
+ rosie/builtin/profiles/blubot/profile.toml,sha256=D89zIzrz9rVUr-3cj3PeNwuIJhMAv29sMauaHVRzqVE,941
20
+ rosie/builtin/profiles/blubot/thumbnail.png,sha256=saM1bispoPWYW__FgY2ITPg9rhC0b6kTkLgQPtf-aiQ,191199
21
+ rosie/builtin/profiles/blubot/vector.svg,sha256=DJ7-hmBQP8EFvS1ixhw5ADQODPscfEc6TVgpkysz6Q0,14552
22
+ rosie/builtin/profiles/kevin/profile.toml,sha256=kMdsepafPcFUve_tYVUZ6l284I6iLT42tDt3OzuKBB8,911
23
+ rosie/builtin/profiles/kevin/thumbnail.png,sha256=PQ4fuHeG4R5aF7RD_8sO4YxGOVfRPEprUJ7Izmw_acE,255660
24
+ rosie/builtin/profiles/kevin/vector.svg,sha256=vxPHNGza0KTRLyyJwYAiXt3ZICdDgUdgUPE5BNYAN-Y,22923
25
+ rosie/builtin/profiles/ltl/profile.toml,sha256=vJ8QG3_k53_UYEBCpVVb0RA3IHeN2UJ8FCYRYMLhMIo,1016
26
+ rosie/builtin/profiles/ltl/thumbnail.png,sha256=jSEunewwtZXtp1-gsdEjfYKMwn9RTFbgX-u8jho86Oo,241946
27
+ rosie/builtin/profiles/ltl/vector.svg,sha256=5Tr1pPbsZDQL8Fyi6gLpLFbSlNHQa5a5eixkg5gWxRA,27707
28
+ rosie/builtin/profiles/rovert/profile.toml,sha256=1QPG1YkJcnySzcnYYCQq_a6MXbAGUo6PYvyW7eqdXZU,946
29
+ rosie/builtin/profiles/rovert/thumbnail.png,sha256=lxWBxfxrqVkvB69_nV5OeTo1T31LRS42IxOv-6beqB8,144212
30
+ rosie/builtin/profiles/rovert/vector.svg,sha256=U8586XwxunzqUSZA23NcxK6O3O9_XeyMRv3zm4N4twQ,9113
31
+ rosie/builtin/profiles/simubot/profile.toml,sha256=-JqF0MzcRGRUGx7sOE9GOUIf1UdUkzHLJLhyFGpb8og,769
32
+ rosie/builtin/profiles/simubot/thumbnail.png,sha256=YCBCae6OodHg5rvotLg1bkJquwDY7V3ybVKZiUwn5R8,56233
33
+ rosie/builtin/profiles/simubot/vector.svg,sha256=p1APWqiuvyo99JZv_WNXmRIr9lTy51GQL2dafQcYNMs,31720
34
+ rosie/client/__init__.py,sha256=KPU4ziUe9wmT2syTEEKnfM3rkRLzKWCUavE_DLh6h7o,434
35
+ rosie/client/api.py,sha256=mxM0vnmZFvbTpIj5fBegeYrQ2Qi39A1g-N3LcDaYQN0,6981
36
+ rosie/client/models.py,sha256=QoKPx0UeSiBiGAAN6s4WPsfHsspXFaFg5Q_SkgdgIpI,629
37
+ rosie/control/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
+ rosie/control/models.py,sha256=LHT17ern9oDmNbSePELLHPvUUlZ8mP4NOt9k_sVU3gU,2278
39
+ rosie/control/movement.py,sha256=T2KnQjHQiAbwKmqCouiHYGWxOyKc2scwbpXPa_By8Cw,4677
40
+ rosie/control/regulation.py,sha256=OHM1jGLZ-ctITAhGr_UF4EdepymipLG8lPR9VokMDnc,1358
41
+ rosie/control/teleoperation.py,sha256=ekLPg8hjVlAR_hvGhXonuFXxoAa9DWVFwoqBRdJBFBs,1081
42
+ rosie/control/tracking.py,sha256=tG9bxO53UzA_Ei4AXH6bvY88DPRwXeAOCH0CiJ-sLgk,1359
43
+ rosie/hardware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
44
+ rosie/hardware/buses.py,sha256=I6p90kOqaHCkel9ph2Ll5Cma3rYWE9lwvypqNfVyKgM,1150
45
+ rosie/hardware/models.py,sha256=7ZEBYOgpGz7JWpikZOqdrHzIKfyzICSJtrs4Xp3SiEo,596
46
+ rosie/hardware/drive/__init__.py,sha256=YNNIWvXKqCIz1vDhHRCiIjYG0TXvPS55dwb3tAzZoIw,677
47
+ rosie/hardware/drive/kinematics.py,sha256=dOuVXp74fK8GT37vpSvFHhSGjjhRainLG2TGEzrKibc,514
48
+ rosie/hardware/drive/models.py,sha256=wC2UrYMIJ8kDQsBA38HhELLBMOEbs6RLIVAnPOJr_PM,1820
49
+ rosie/hardware/drive/pid.py,sha256=6CgK7vJNasm0ljDI_4Wnvj7Jy3qjpTBXWq4PhIxznvQ,800
50
+ rosie/hardware/drive/devices/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
51
+ rosie/hardware/drive/devices/arduino.py,sha256=rT1Apl50oi-ubnrAlsY2NInFSJhPapZhZQzEaypmyrk,3390
52
+ rosie/hardware/drive/devices/md25.py,sha256=_M-1zvv0Rm1kjOE6pKPpLYGFtkfH9S42AsqEVTEFCEk,5839
53
+ rosie/hardware/drive/devices/virtual.py,sha256=gF2En6t3dPYt5XCWljM_FNSBqpwt-5mf2HuDi1hf3UY,2588
54
+ rosie/hardware/sensors/__init__.py,sha256=gIexC6Hocuv6NKNTuHRasuP2-tZNPZLBPzXNvAs6NWs,806
55
+ rosie/hardware/sensors/models.py,sha256=zaIGiXWruSBg_-zSFmHFXCg14_tUphHD2npSLxR7yy4,349
56
+ rosie/hardware/sensors/usound.py,sha256=Zglmv6LmoM0MGXjBhlRDMPIBMOUVO0ZTrt7qEK8G2mc,7977
57
+ rosie/localization/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
58
+ rosie/localization/odometry.py,sha256=w_guiEeN1XdhYLxO5RolnzZMaPj1KZCpPnBvAdsOhjo,1480
59
+ rosie/planning/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
60
+ rosie/planning/models.py,sha256=aM8rZs-UZ7j2big2etSNhGFUOD1cc0oAUhYrSejUx70,1181
61
+ rosie/planning/path.py,sha256=DZCxQxOavYktY798KsiaZ1l97qYhEH7701FsYSUE3aA,1156
62
+ rosie/planning/search.py,sha256=BJS-FR-ZfMpdTd65-QM0kpLZLe_K3lBaSq-Y6GZPHu8,1640
63
+ rosie/planning/trajectory.py,sha256=ULxYW5tyrqaLQ658gLGqHZC99wCgRJaQ2tLeAySX_YQ,6284
64
+ rosie/planning/visibility.py,sha256=gBjtMGGNCPGngZGmD2oDPI8QnFUPMoE3bJR39A2LfO4,2830
65
+ rosie/world/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
66
+ rosie/world/maps.py,sha256=zUmKwgZIRVueodvBB8KYhHGSGt9K7zGmzi7sKe-hi9U,2742
67
+ rosie/world/models.py,sha256=MYPWSX_1ITQtkE5wJnrmiQVTDjq_kc-ANCIt6qEOBJ0,444
68
+ pyforce_rosie-2.0.0.dist-info/METADATA,sha256=lxuB1aSOnqm8eBk8O_pmx3XKBz0zOUuGwmwwP_SNAnY,7422
69
+ pyforce_rosie-2.0.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
70
+ pyforce_rosie-2.0.0.dist-info/entry_points.txt,sha256=NAs606q0hysPA4fQ5U2ICf2PxAaqE4JiHVsgfVLiyOg,46
71
+ pyforce_rosie-2.0.0.dist-info/licenses/LICENSE,sha256=besggTlfcnF02jOikkoAnanhiOOyHM7tf_sAqkvnjYM,1083
72
+ pyforce_rosie-2.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ rosie = rosie.__main__:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2015 The rosie authors (PyForce)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
rosie/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "2.0.0"
rosie/__main__.py ADDED
@@ -0,0 +1,17 @@
1
+ import importlib
2
+ import sys
3
+
4
+
5
+ def main() -> None:
6
+ try:
7
+ # the command needs the `robot` extra; the client alone doesn't
8
+ cli = importlib.import_module("rosie.cli")
9
+ except ModuleNotFoundError as error:
10
+ sys.exit(
11
+ f"rosie needs the robot: pip install 'pyforce-rosie[robot]' (missing {error.name})"
12
+ )
13
+ cli.app()
14
+
15
+
16
+ if __name__ == "__main__":
17
+ main()
rosie/apis/__init__.py ADDED
File without changes
rosie/apis/models.py ADDED
@@ -0,0 +1,11 @@
1
+ from collections.abc import Callable
2
+ from contextlib import AbstractAsyncContextManager
3
+ from typing import TYPE_CHECKING
4
+
5
+ from rosie.models import StartSettings
6
+
7
+ if TYPE_CHECKING:
8
+ from rosie.core import Robot
9
+
10
+ # an interface serves the robot while inside its context
11
+ type Interface = Callable[[Robot, StartSettings], AbstractAsyncContextManager[None]]
File without changes
@@ -0,0 +1,57 @@
1
+ from typing import Literal
2
+
3
+ from pydantic import BaseModel, ConfigDict
4
+
5
+
6
+ class _Frozen(BaseModel):
7
+ model_config = ConfigDict(frozen=True)
8
+
9
+
10
+ class Position(_Frozen):
11
+ x: float # m
12
+ y: float # m
13
+ theta: float # rad
14
+
15
+
16
+ class GotoOrder(_Frozen):
17
+ target: tuple[float, float] # x, y (m)
18
+ speed: float # m/s
19
+ planner: bool = False # go around walls and furniture
20
+
21
+
22
+ class FollowOrder(_Frozen):
23
+ path: list[tuple[float, float]] # points to drive through (m)
24
+ speed: float # m/s
25
+ planner: bool = False # go around walls and furniture from point to point
26
+
27
+
28
+ class RobotInfo(_Frozen):
29
+ name: str
30
+ thumbnail: str # path of the robot's picture
31
+ vector: str # path of the robot's icon
32
+ video: str | Literal[False] # video stream (":port/path" on the robot's host), if any
33
+ size: tuple[float, float, float] # length, width, height (m)
34
+ profile: str
35
+
36
+
37
+ class Maps(_Frozen):
38
+ maps: list[str]
39
+
40
+
41
+ class TrajectoryPlanners(_Frozen):
42
+ planners: list[str]
43
+ in_use: str
44
+
45
+
46
+ class DriveCommand(_Frozen):
47
+ """WebSocket, client to rosie: the driving commands under manual control."""
48
+
49
+ type: Literal["move"]
50
+ data: tuple[float, float, float] # steering, throttle, spin, each in [-1, 1]
51
+
52
+
53
+ class PositionUpdate(_Frozen):
54
+ """WebSocket, rosie to clients: where the robot is now."""
55
+
56
+ type: Literal["position"]
57
+ data: Position
@@ -0,0 +1,228 @@
1
+ import asyncio
2
+ import json
3
+ import logging
4
+ from typing import Annotated, Any
5
+
6
+ from fastapi import APIRouter, Depends, HTTPException, Request, WebSocket, WebSocketDisconnect
7
+ from fastapi.responses import FileResponse, JSONResponse, Response
8
+ from pydantic import ValidationError
9
+
10
+ from rosie.apis.web.models import (
11
+ DriveCommand,
12
+ FollowOrder,
13
+ GotoOrder,
14
+ Maps,
15
+ Position,
16
+ PositionUpdate,
17
+ RobotInfo,
18
+ TrajectoryPlanners,
19
+ )
20
+ from rosie.control.models import Telemetry
21
+ from rosie.core import Robot
22
+ from rosie.errors import NoRouteError
23
+ from rosie.planning.trajectory import TRAJECTORY_PLANNERS
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ router = APIRouter()
28
+
29
+
30
+ def get_robot(request: Request) -> Robot:
31
+ robot: Robot = request.app.state.robot
32
+ return robot
33
+
34
+
35
+ RobotDependency = Annotated[Robot, Depends(get_robot)]
36
+
37
+
38
+ @router.get("/odometry")
39
+ def odometry(robot: RobotDependency) -> Position:
40
+ x, y, theta = robot.position()
41
+ return Position(x=x, y=y, theta=theta)
42
+
43
+
44
+ @router.get("/metadata")
45
+ def metadata(request: Request, robot: RobotDependency) -> RobotInfo:
46
+ profile = robot.profile
47
+ return RobotInfo(
48
+ name=profile.name,
49
+ thumbnail=request.app.url_path_for("thumbnail"),
50
+ vector=request.app.url_path_for("vector"),
51
+ video=profile.camera.stream if profile.camera else False,
52
+ size=(profile.body.length, profile.body.width, profile.body.height),
53
+ profile=profile.folder_name,
54
+ )
55
+
56
+
57
+ @router.get("/thumbnail")
58
+ def thumbnail(robot: RobotDependency) -> FileResponse:
59
+ return FileResponse(robot.profile.picture)
60
+
61
+
62
+ @router.get("/vector")
63
+ def vector(robot: RobotDependency) -> FileResponse:
64
+ return FileResponse(robot.profile.icon)
65
+
66
+
67
+ @router.get("/sensor/{name}")
68
+ def sensor(name: str, robot: RobotDependency) -> dict[str, object]:
69
+ if name not in robot.sensors:
70
+ raise HTTPException(status_code=404, detail=f"no sensor {name}")
71
+ return {name: robot.sensors[name]()}
72
+
73
+
74
+ @router.post("/position")
75
+ async def position(request: Request, robot: RobotDependency) -> Response:
76
+ """Teleport the robot."""
77
+ target = Position.model_validate_json(await request.body())
78
+ robot.teleport(target.x, target.y, target.theta)
79
+ return Response()
80
+
81
+
82
+ @router.post("/goto")
83
+ async def goto(request: Request, robot: RobotDependency) -> Response:
84
+ order = GotoOrder.model_validate_json(await request.body())
85
+ x, y = order.target
86
+ try:
87
+ if order.planner:
88
+ robot.follow_with_planner([(x, y)], order.speed)
89
+ else:
90
+ robot.goto(x, y, order.speed)
91
+ except NoRouteError as error:
92
+ return JSONResponse({"error": str(error)}, status_code=409)
93
+ except ValueError as error:
94
+ return JSONResponse({"detail": str(error)}, status_code=422)
95
+ return Response()
96
+
97
+
98
+ @router.post("/follow")
99
+ async def follow(request: Request, robot: RobotDependency) -> Response:
100
+ order = FollowOrder.model_validate_json(await request.body())
101
+ try:
102
+ if order.planner:
103
+ robot.follow_with_planner(order.path, order.speed)
104
+ else:
105
+ robot.follow(order.path, order.speed)
106
+ except NoRouteError as error:
107
+ return JSONResponse({"error": str(error)}, status_code=409)
108
+ except ValueError as error:
109
+ return JSONResponse({"detail": str(error)}, status_code=422)
110
+ return Response()
111
+
112
+
113
+ @router.get("/maps")
114
+ def maps(robot: RobotDependency) -> Maps:
115
+ return Maps(maps=list(robot.maps()))
116
+
117
+
118
+ @router.get("/map")
119
+ def current_map(robot: RobotDependency) -> dict[str, Any]:
120
+ """The map the planner uses."""
121
+ worldmap = robot.get_map(robot.map_name) if robot.map_name else None
122
+ if not worldmap:
123
+ raise HTTPException(status_code=404)
124
+ return worldmap
125
+
126
+
127
+ @router.get("/map/{name}")
128
+ def getmap(name: str, robot: RobotDependency) -> dict[str, Any]:
129
+ worldmap = robot.get_map(name)
130
+ if not worldmap:
131
+ raise HTTPException(status_code=404)
132
+ return worldmap
133
+
134
+
135
+ @router.post("/map/{name}")
136
+ def use_map(name: str, robot: RobotDependency) -> Response:
137
+ """Plan routes on this map from now on."""
138
+ if name not in robot.maps():
139
+ raise HTTPException(status_code=404, detail=f"no map {name}")
140
+ robot.use_map(name)
141
+ return Response()
142
+
143
+
144
+ @router.get("/trajectory-planners")
145
+ def trajectory_planners(robot: RobotDependency) -> TrajectoryPlanners:
146
+ return TrajectoryPlanners(planners=list(TRAJECTORY_PLANNERS), in_use=robot.trajectory_planner)
147
+
148
+
149
+ @router.post("/trajectory-planner/{name}")
150
+ def use_trajectory_planner(name: str, robot: RobotDependency) -> Response:
151
+ """Plan the next movements with this trajectory planner."""
152
+ if name not in TRAJECTORY_PLANNERS:
153
+ raise HTTPException(status_code=404, detail=f"no trajectory planner {name}")
154
+ robot.use_trajectory_planner(name)
155
+ return Response()
156
+
157
+
158
+ class WebObserver:
159
+ """Streams the robot's position to WebSocket clients, and takes their commands in manual mode."""
160
+
161
+ def __init__(self, robot: Robot) -> None:
162
+ self.robot = robot
163
+ self.is_manual = False
164
+ self.clients: list[WebSocket] = []
165
+ # pending sends, kept until done (the event loop only keeps weak references)
166
+ self.sending: set[asyncio.Task[None]] = set()
167
+ self.last_location = (0.0, 0.0, 0.0)
168
+
169
+ async def websocket(self, websocket: WebSocket) -> None:
170
+ await websocket.accept()
171
+ logger.debug("connected websocket client: %s", websocket.client)
172
+ self.clients.append(websocket)
173
+ try:
174
+ while True:
175
+ self._receive(await websocket.receive_text())
176
+ except WebSocketDisconnect:
177
+ pass
178
+ finally:
179
+ self.clients.remove(websocket)
180
+
181
+ def movement_begin(self, expected_updates: int | None) -> None:
182
+ pass
183
+
184
+ def movement_end(self) -> None:
185
+ pass
186
+
187
+ def movement_update(self, telemetry: Telemetry) -> None:
188
+ pose = telemetry.pose
189
+ location = (pose.x, pose.y, pose.theta)
190
+ if location == self.last_location:
191
+ return
192
+ self.last_location = location
193
+
194
+ message = PositionUpdate(
195
+ type="position", data=Position(x=pose.x, y=pose.y, theta=pose.theta)
196
+ ).model_dump_json()
197
+ for client in list(self.clients):
198
+ task = asyncio.create_task(client.send_text(message))
199
+ self.sending.add(task)
200
+ task.add_done_callback(self._sent)
201
+
202
+ def manual(self) -> Response:
203
+ self.is_manual = True
204
+ self.robot.start_manual()
205
+ return Response()
206
+
207
+ def planned(self) -> Response:
208
+ self.is_manual = False
209
+ self.robot.stop_manual()
210
+ return Response()
211
+
212
+ def _receive(self, text: str) -> None:
213
+ message = json.loads(text)
214
+ if not isinstance(message, dict) or message.get("type") != "move" or not self.is_manual:
215
+ return
216
+ try:
217
+ move = DriveCommand.model_validate(message)
218
+ except ValidationError:
219
+ logger.warning("ignoring malformed move: %s", text)
220
+ return
221
+ steering, throttle, spin = move.data
222
+ self.robot.move(steering, throttle, spin)
223
+
224
+ def _sent(self, task: asyncio.Task[None]) -> None:
225
+ """Forget a finished send; a client gone meanwhile is not an error."""
226
+ self.sending.discard(task)
227
+ if not task.cancelled():
228
+ task.exception()
@@ -0,0 +1,59 @@
1
+ import asyncio
2
+ import logging
3
+ from collections.abc import AsyncGenerator
4
+ from contextlib import asynccontextmanager
5
+
6
+ import uvicorn
7
+ from fastapi import FastAPI, Request
8
+ from fastapi.middleware.cors import CORSMiddleware
9
+ from fastapi.responses import Response
10
+ from pydantic import ValidationError
11
+
12
+ from rosie.apis.web import routes
13
+ from rosie.core import Robot
14
+ from rosie.models import StartSettings
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ @asynccontextmanager
20
+ async def start_web_api(robot: Robot, settings: StartSettings) -> AsyncGenerator[None, None]:
21
+ """Start the web API; on the way out of the block, uvicorn closes its connections."""
22
+ server = uvicorn.Server(
23
+ uvicorn.Config(
24
+ create_app(robot), host=settings.host, port=settings.port, log_level="warning"
25
+ )
26
+ )
27
+ serving = asyncio.create_task(server.serve())
28
+ logger.info("web API on http://%s:%d", settings.host, settings.port)
29
+ try:
30
+ yield
31
+ finally:
32
+ server.should_exit = True
33
+ await serving
34
+
35
+
36
+ def create_app(robot: Robot) -> FastAPI:
37
+ """REST + WebSocket interface to `robot`."""
38
+ app = FastAPI(title="rosie")
39
+ # WebRosie is served from another origin than the robot
40
+ app.add_middleware(
41
+ CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]
42
+ )
43
+
44
+ @app.exception_handler(ValidationError)
45
+ async def invalid_request(_request: Request, error: ValidationError) -> Response:
46
+ # error.json() also serialises raw input that isn't valid JSON (bytes)
47
+ detail = error.json(include_url=False)
48
+ return Response(f'{{"detail":{detail}}}', status_code=422, media_type="application/json")
49
+
50
+ app.state.robot = robot
51
+ app.include_router(routes.router)
52
+
53
+ observer = routes.WebObserver(robot)
54
+ robot.observers.append(observer)
55
+ # bound methods can't use the decorators
56
+ app.add_api_websocket_route("/websocket", observer.websocket)
57
+ app.add_api_route("/manual", observer.manual, methods=["PUT", "POST"])
58
+ app.add_api_route("/planned", observer.planned, methods=["PUT", "POST"])
59
+ return app