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.
- pyforce_rosie-2.0.0.dist-info/METADATA +166 -0
- pyforce_rosie-2.0.0.dist-info/RECORD +72 -0
- pyforce_rosie-2.0.0.dist-info/WHEEL +4 -0
- pyforce_rosie-2.0.0.dist-info/entry_points.txt +2 -0
- pyforce_rosie-2.0.0.dist-info/licenses/LICENSE +21 -0
- rosie/__init__.py +1 -0
- rosie/__main__.py +17 -0
- rosie/apis/__init__.py +0 -0
- rosie/apis/models.py +11 -0
- rosie/apis/web/__init__.py +0 -0
- rosie/apis/web/models.py +57 -0
- rosie/apis/web/routes.py +228 -0
- rosie/apis/web/server.py +59 -0
- rosie/builtin/maps/house.geojson +23 -0
- rosie/builtin/profiles/blubot/profile.toml +40 -0
- rosie/builtin/profiles/blubot/thumbnail.png +0 -0
- rosie/builtin/profiles/blubot/vector.svg +177 -0
- rosie/builtin/profiles/kevin/profile.toml +39 -0
- rosie/builtin/profiles/kevin/thumbnail.png +0 -0
- rosie/builtin/profiles/kevin/vector.svg +253 -0
- rosie/builtin/profiles/ltl/profile.toml +42 -0
- rosie/builtin/profiles/ltl/thumbnail.png +0 -0
- rosie/builtin/profiles/ltl/vector.svg +217 -0
- rosie/builtin/profiles/rovert/profile.toml +40 -0
- rosie/builtin/profiles/rovert/thumbnail.png +0 -0
- rosie/builtin/profiles/rovert/vector.svg +115 -0
- rosie/builtin/profiles/simubot/profile.toml +32 -0
- rosie/builtin/profiles/simubot/thumbnail.png +0 -0
- rosie/builtin/profiles/simubot/vector.svg +232 -0
- rosie/cli.py +96 -0
- rosie/client/__init__.py +16 -0
- rosie/client/api.py +183 -0
- rosie/client/models.py +22 -0
- rosie/control/__init__.py +0 -0
- rosie/control/models.py +72 -0
- rosie/control/movement.py +136 -0
- rosie/control/regulation.py +34 -0
- rosie/control/teleoperation.py +28 -0
- rosie/control/tracking.py +42 -0
- rosie/core.py +216 -0
- rosie/errors.py +19 -0
- rosie/hardware/__init__.py +0 -0
- rosie/hardware/buses.py +37 -0
- rosie/hardware/drive/__init__.py +19 -0
- rosie/hardware/drive/devices/__init__.py +0 -0
- rosie/hardware/drive/devices/arduino.py +96 -0
- rosie/hardware/drive/devices/md25.py +157 -0
- rosie/hardware/drive/devices/virtual.py +69 -0
- rosie/hardware/drive/kinematics.py +12 -0
- rosie/hardware/drive/models.py +63 -0
- rosie/hardware/drive/pid.py +22 -0
- rosie/hardware/models.py +19 -0
- rosie/hardware/sensors/__init__.py +19 -0
- rosie/hardware/sensors/models.py +12 -0
- rosie/hardware/sensors/usound.py +207 -0
- rosie/localization/__init__.py +0 -0
- rosie/localization/odometry.py +40 -0
- rosie/models.py +144 -0
- rosie/planning/__init__.py +0 -0
- rosie/planning/models.py +36 -0
- rosie/planning/path.py +25 -0
- rosie/planning/search.py +47 -0
- rosie/planning/trajectory.py +156 -0
- rosie/planning/visibility.py +68 -0
- rosie/profile.py +91 -0
- rosie/py.typed +0 -0
- rosie/recording.py +102 -0
- rosie/robot.py +51 -0
- rosie/service.py +86 -0
- rosie/world/__init__.py +0 -0
- rosie/world/maps.py +69 -0
- 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
|
+
[](https://github.com/PyForce/rosie/actions/workflows/qa.yml)
|
|
34
|
+
[](https://github.com/PyForce/rosie/releases/latest)
|
|
35
|
+
[](https://pypi.org/project/pyforce-rosie/)
|
|
36
|
+
[](https://github.com/PyForce/rosie/pkgs/container/rosie)
|
|
37
|
+
[](https://pyforce-rosie.readthedocs.io)
|
|
38
|
+
[](https://www.python.org/)
|
|
39
|
+
[](https://github.com/PyForce/rosie/blob/main/LICENSE)
|
|
40
|
+
<br>
|
|
41
|
+
[](https://github.com/astral-sh/uv)
|
|
42
|
+
[](https://github.com/astral-sh/ruff)
|
|
43
|
+
[](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
|
+

|
|
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,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
|
rosie/apis/web/models.py
ADDED
|
@@ -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
|
rosie/apis/web/routes.py
ADDED
|
@@ -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()
|
rosie/apis/web/server.py
ADDED
|
@@ -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
|