bonicos 0.1.0__tar.gz
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.
- bonicos-0.1.0/.flake8 +7 -0
- bonicos-0.1.0/.gitignore +9 -0
- bonicos-0.1.0/API.md +415 -0
- bonicos-0.1.0/LICENSE +21 -0
- bonicos-0.1.0/PKG-INFO +170 -0
- bonicos-0.1.0/PROTOCOL.md +426 -0
- bonicos-0.1.0/README.md +126 -0
- bonicos-0.1.0/bonicos/__init__.py +32 -0
- bonicos-0.1.0/bonicos/controllers/__init__.py +27 -0
- bonicos-0.1.0/bonicos/controllers/_base.py +54 -0
- bonicos-0.1.0/bonicos/controllers/arm.py +254 -0
- bonicos-0.1.0/bonicos/controllers/camera.py +61 -0
- bonicos-0.1.0/bonicos/controllers/head.py +69 -0
- bonicos-0.1.0/bonicos/controllers/motion.py +96 -0
- bonicos-0.1.0/bonicos/controllers/navigation.py +256 -0
- bonicos-0.1.0/bonicos/controllers/precise_motion.py +182 -0
- bonicos-0.1.0/bonicos/controllers/sensors.py +93 -0
- bonicos-0.1.0/bonicos/controllers/system.py +129 -0
- bonicos-0.1.0/bonicos/discovery.py +76 -0
- bonicos-0.1.0/bonicos/enums.py +57 -0
- bonicos-0.1.0/bonicos/exceptions.py +51 -0
- bonicos-0.1.0/bonicos/protocol.py +242 -0
- bonicos-0.1.0/bonicos/py.typed +0 -0
- bonicos-0.1.0/bonicos/robot.py +509 -0
- bonicos-0.1.0/bonicos/transports/__init__.py +12 -0
- bonicos-0.1.0/bonicos/transports/_camera_link.py +189 -0
- bonicos-0.1.0/bonicos/transports/base.py +89 -0
- bonicos-0.1.0/bonicos/transports/mock.py +151 -0
- bonicos-0.1.0/bonicos/transports/sim.py +360 -0
- bonicos-0.1.0/bonicos/transports/websocket.py +300 -0
- bonicos-0.1.0/bonicos.egg-info/PKG-INFO +170 -0
- bonicos-0.1.0/bonicos.egg-info/SOURCES.txt +61 -0
- bonicos-0.1.0/bonicos.egg-info/dependency_links.txt +1 -0
- bonicos-0.1.0/bonicos.egg-info/requires.txt +16 -0
- bonicos-0.1.0/bonicos.egg-info/top_level.txt +1 -0
- bonicos-0.1.0/examples/01_connect_and_lifecycle.py +33 -0
- bonicos-0.1.0/examples/02_basic_motion.py +50 -0
- bonicos-0.1.0/examples/03_precise_motion_and_queue.py +50 -0
- bonicos-0.1.0/examples/04_navigation_and_mapping.py +116 -0
- bonicos-0.1.0/examples/05_arms_grippers_neck.py +75 -0
- bonicos-0.1.0/examples/06_head_expression_and_display.py +41 -0
- bonicos-0.1.0/examples/07_speech.py +25 -0
- bonicos-0.1.0/examples/08_sensors_and_telemetry.py +42 -0
- bonicos-0.1.0/examples/09_system_and_health.py +52 -0
- bonicos-0.1.0/examples/10_square_patrol_routine.py +50 -0
- bonicos-0.1.0/examples/11_error_handling.py +70 -0
- bonicos-0.1.0/examples/README.md +58 -0
- bonicos-0.1.0/pyproject.toml +118 -0
- bonicos-0.1.0/setup.cfg +4 -0
- bonicos-0.1.0/tests/__init__.py +0 -0
- bonicos-0.1.0/tests/conftest.py +48 -0
- bonicos-0.1.0/tests/test_arm.py +221 -0
- bonicos-0.1.0/tests/test_camera.py +87 -0
- bonicos-0.1.0/tests/test_connect_resolution.py +172 -0
- bonicos-0.1.0/tests/test_motion.py +84 -0
- bonicos-0.1.0/tests/test_navigation.py +182 -0
- bonicos-0.1.0/tests/test_precise_motion.py +112 -0
- bonicos-0.1.0/tests/test_protocol.py +25 -0
- bonicos-0.1.0/tests/test_robot_lifecycle.py +75 -0
- bonicos-0.1.0/tests/test_sensors.py +69 -0
- bonicos-0.1.0/tests/test_sim_transport.py +162 -0
- bonicos-0.1.0/tests/test_system.py +146 -0
- bonicos-0.1.0/tests/test_websocket_transport.py +102 -0
bonicos-0.1.0/.flake8
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
[flake8]
|
|
2
|
+
max-line-length = 88
|
|
3
|
+
# black's formatting intentionally conflicts with these two pycodestyle
|
|
4
|
+
# rules (slice whitespace, line-break-before-binary-operator) — standard
|
|
5
|
+
# ignore list for repos using black + flake8 together (matches
|
|
6
|
+
# [tool.black] line-length in pyproject.toml).
|
|
7
|
+
extend-ignore = E203, W503
|
bonicos-0.1.0/.gitignore
ADDED
bonicos-0.1.0/API.md
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
# bonicos — User-Facing Python API
|
|
2
|
+
|
|
3
|
+
The complete method reference. Synchronous and blocking — no `async`/`await`.
|
|
4
|
+
Identical whether your code runs on your own machine or on the robot itself.
|
|
5
|
+
Wire details live in [PROTOCOL.md](./PROTOCOL.md).
|
|
6
|
+
|
|
7
|
+
> **Marker:** methods tagged **🔌 stub** exist and are safe to call but are
|
|
8
|
+
> **no-ops on current robot firmware** — the robot accepts the command and
|
|
9
|
+
> does nothing. Your code runs; that actuator just won't move until the robot
|
|
10
|
+
> side lands. Everything else is fully functional.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 1. Connect & lifecycle
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from bonicos import BonicBot
|
|
18
|
+
|
|
19
|
+
# Explicit host (developer laptop → robot on the LAN, or → tablet on lite models)
|
|
20
|
+
robot = BonicBot("192.168.1.50", robot_id="M1_001")
|
|
21
|
+
|
|
22
|
+
# Everywhere the environment already knows which robot this is:
|
|
23
|
+
# the on-robot runner ($BONICOS_HOST/$BONICOS_ROBOT_ID),
|
|
24
|
+
# or mDNS autodiscovery.
|
|
25
|
+
robot = BonicBot()
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**A bare `BonicBot()` is a working program wherever the environment already
|
|
29
|
+
knows which robot you mean** — which is what lets the same file run unchanged
|
|
30
|
+
on your laptop and on the robot itself. It looks for a target in this order:
|
|
31
|
+
|
|
32
|
+
| Order | Source | Typical use |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| 1 | the `host` / `robot_id` arguments | you, naming a robot from your own machine |
|
|
35
|
+
| 2 | `$BONICOS_HOST` / `$BONICOS_ROBOT_ID` | set for you when your code runs on the robot |
|
|
36
|
+
| 3 | mDNS autodiscovery | `pip install bonicos[discovery]` |
|
|
37
|
+
|
|
38
|
+
| Method | Blocks? | Description |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| `BonicBot(host=None, *, robot_id=None, token=None, timeout=10.0)` | yes (connects) | Connect + handshake, resolving the target per the table above. `token` defaults to `$BONICOS_TOKEN`. Raises `ConnectionError` on failure, naming every way to supply what was missing. |
|
|
41
|
+
| `robot.is_connected() -> bool` | no | Live connection state. |
|
|
42
|
+
| `robot.close()` | yes | Stop the robot, close the transport. Idempotent. |
|
|
43
|
+
| `robot.features -> dict[str, bool]` | no | Series feature flags from the handshake (e.g. `robot.features["navigation"]`). |
|
|
44
|
+
| `robot.robot_id -> str`, `robot.series -> str` | no | From the handshake. |
|
|
45
|
+
|
|
46
|
+
### Trying it without hardware
|
|
47
|
+
|
|
48
|
+
`BonicBot.simulated()` connects to a fake robot instead — no network, no
|
|
49
|
+
physical robot required:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
robot = BonicBot.simulated()
|
|
53
|
+
robot.move_forward(speed=0.3, duration=2)
|
|
54
|
+
print(robot.get_position()) # a real integrated pose, not a stub value
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Driving, arms, and telemetry all behave for real — `get_servo_angles()`
|
|
58
|
+
converges the same way it would against hardware. Navigation and mapping
|
|
59
|
+
calls ack and do nothing (no Nav2/SLAM is simulated), the same as their
|
|
60
|
+
**🔌 stub** counterparts on real firmware. There's no separate API to learn:
|
|
61
|
+
every method on this page works identically against `BonicBot.simulated()`
|
|
62
|
+
and against a real robot.
|
|
63
|
+
|
|
64
|
+
Supports the context-manager form, which guarantees a `stop` on exit
|
|
65
|
+
(recommended for every run — matches the platform "stop in a `finally`" rule):
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
with BonicBot("192.168.1.50", robot_id="M1_001") as robot:
|
|
69
|
+
robot.move_forward(duration=2)
|
|
70
|
+
# motors stopped, socket closed, even on exception
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Calling a **gated** feature raises `FeatureUnavailable`; a disconnect mid-call
|
|
74
|
+
raises `RobotDisconnected`.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## 2. Motion (base movement)
|
|
79
|
+
|
|
80
|
+
High-level wrappers over the `drive` command. `duration=None` starts the motion
|
|
81
|
+
and returns immediately; a number blocks for that many seconds then stops.
|
|
82
|
+
|
|
83
|
+
| Method | Description |
|
|
84
|
+
|---|---|
|
|
85
|
+
| `robot.drive(linear_x=0.0, angular_z=0.0)` | Raw velocity (m/s, rad/s). Sent continuously by the SDK to satisfy the deadman while active. |
|
|
86
|
+
| `robot.move_forward(speed=0.3, duration=None)` | Forward at `speed` m/s. |
|
|
87
|
+
| `robot.move_backward(speed=0.3, duration=None)` | Backward. |
|
|
88
|
+
| `robot.turn_left(speed=0.5, duration=None)` | Rotate left at `speed` rad/s. |
|
|
89
|
+
| `robot.turn_right(speed=0.5, duration=None)` | Rotate right. |
|
|
90
|
+
| `robot.stop()` | Zero velocity immediately. |
|
|
91
|
+
| `robot.is_moving() -> bool` | From odom telemetry. |
|
|
92
|
+
|
|
93
|
+
Grouped access: `robot.motion.*` (same methods).
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 3. Precise motion (closed-loop)
|
|
98
|
+
|
|
99
|
+
Client-side control loops over `drive` + odometry. **Blocking**
|
|
100
|
+
with a timeout; the on-robot deadman backstops a stalled loop.
|
|
101
|
+
|
|
102
|
+
| Method | Description |
|
|
103
|
+
|---|---|
|
|
104
|
+
| `robot.drive_distance(meters, speed=0.3, timeout=30.0) -> bool` | Drive straight a set distance. `True` on success. |
|
|
105
|
+
| `robot.rotate_angle(degrees, speed=45.0, timeout=30.0) -> bool` | Rotate in place by an angle. |
|
|
106
|
+
| `robot.drive_and_rotate(meters, degrees, speed=0.3, turn_speed=45.0, timeout=30.0) -> bool` | Drive then rotate. |
|
|
107
|
+
| `robot.draw_square(side_m, speed=0.3, turn_speed=45.0) -> bool` | Convenience pattern. |
|
|
108
|
+
|
|
109
|
+
Command queue (build a routine, then run it):
|
|
110
|
+
|
|
111
|
+
| Method | Description |
|
|
112
|
+
|---|---|
|
|
113
|
+
| `robot.enqueue(cmd_list)` | Queue precise-motion steps, e.g. `[("drive", 1.0), ("rotate", 90)]`. |
|
|
114
|
+
| `robot.run_queue(block=True) -> bool` | Execute the queue. |
|
|
115
|
+
| `robot.clear_queue()` | Flush queue and stop. |
|
|
116
|
+
|
|
117
|
+
> These signatures are frozen across the v1→on-Pi migration: when the robot side
|
|
118
|
+
> gains a native motion server, the loop is replaced by a single intent command
|
|
119
|
+
> with no change to these method signatures (PROTOCOL §4, pattern 3 → later 2).
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 4. Navigation, mapping & locations
|
|
124
|
+
|
|
125
|
+
Fire-and-monitor: goal methods start navigation; `wait_for_goal()` blocks on
|
|
126
|
+
`nav_status`. Coordinates are map-frame meters/radians.
|
|
127
|
+
|
|
128
|
+
| Method | Blocks? | Description |
|
|
129
|
+
|---|---|---|
|
|
130
|
+
| `robot.go_to(x, y, theta=0.0, wait=True, timeout=60.0) -> bool` | `wait` | Navigate to a pose (Nav2). |
|
|
131
|
+
| `robot.navigate_waypoints(points, wait=True) -> bool` | `wait` | `points=[(x,y,theta?), ...]`. |
|
|
132
|
+
| `robot.cancel_goal() -> bool` | yes | Cancel current navigation. |
|
|
133
|
+
| `robot.wait_for_goal(timeout=30.0) -> bool` | yes | Block until the active goal finishes. |
|
|
134
|
+
| `robot.get_nav_status() -> str` | no | `idle`/`navigating`/`succeeded`/`failed`/`canceled`. |
|
|
135
|
+
| `robot.get_distance_to_goal() -> float` | no | From `nav_status`. |
|
|
136
|
+
| `robot.get_plan() -> list[(x, y)]` | no | Latest planned path. Reflects whatever Nav2 last published on `/plan` — verified against the real M1 sim (2026-08-04) that this is **not** guaranteed to clear to `[]` when a goal succeeds (Nav2 just stops updating it, leaving the last path cached); use `get_nav_status()`, not an empty plan, to detect "no longer navigating." |
|
|
137
|
+
| `robot.set_initial_pose(x, y, theta=0.0) -> bool` | yes | Seed localization. |
|
|
138
|
+
| `robot.start_navigation() / stop_navigation() -> bool` | yes | Nav2 lifecycle. **🔌 stub.** |
|
|
139
|
+
|
|
140
|
+
**Nav-mode session switching** (added 2026-08-08) — brings up or tears down
|
|
141
|
+
the whole mapping/navigation ROS launch tree; distinct from `start_mapping`/
|
|
142
|
+
`stop_mapping` below, which only pause/unpause SLAM *inside* an already-
|
|
143
|
+
entered mapping session. Slow (multi-second launch settle), hence the longer
|
|
144
|
+
default timeouts:
|
|
145
|
+
|
|
146
|
+
| Method | Blocks? | Description |
|
|
147
|
+
|---|---|---|
|
|
148
|
+
| `robot.enter_mapping_mode(timeout=30.0) -> bool` | yes | Tear down any nav session, launch slam_toolbox+Nav2. |
|
|
149
|
+
| `robot.enter_navigation_mode(name, timeout=30.0) -> bool` | yes | Tear down any nav session, launch map_server+AMCL+Nav2 localizing on saved map `name`. `False` if the map doesn't exist or the launch fails to come up. |
|
|
150
|
+
| `robot.stop_nav_mode(timeout=15.0) -> bool` | yes | Tear down the current nav session → idle. Drive/sensors stay up. |
|
|
151
|
+
| `robot.get_nav_mode() -> dict` | yes (fresh query) | `{"mode": "idle"\|"mapping"\|"navigating", "map": str\|None, "transitioning": bool, "localized": bool}`. `localized` is freshness-checked (from `pose` staleness), not latched — a robot can be `navigating` and still `localized: False` right after entering (AMCL's seed hasn't landed) or later if it loses the pose. |
|
|
152
|
+
|
|
153
|
+
Typical mapping-then-navigating workflow: `enter_mapping_mode()` →
|
|
154
|
+
`start_mapping()` → drive around → `stop_mapping()` → `save_map(name)` →
|
|
155
|
+
`enter_navigation_mode(name)`.
|
|
156
|
+
|
|
157
|
+
**AMCL seeding is automatic** (added 2026-08-09): `enter_navigation_mode` and
|
|
158
|
+
`load_map` both auto-seed AMCL server-side (the last pose remembered on that
|
|
159
|
+
map, or its origin if new) — you don't need to call `set_initial_pose`
|
|
160
|
+
yourself in the common case. It can still fail on a slow host or a map with
|
|
161
|
+
no remembered pose; poll `get_nav_mode()["localized"]` and fall back to
|
|
162
|
+
`set_initial_pose` if it stays `False`.
|
|
163
|
+
|
|
164
|
+
Mapping:
|
|
165
|
+
|
|
166
|
+
| Method | Description |
|
|
167
|
+
|---|---|
|
|
168
|
+
| `robot.start_mapping() / stop_mapping() -> bool` | Pause/unpause SLAM integration within an already-entered mapping session (see `enter_mapping_mode` above — this alone doesn't launch anything). |
|
|
169
|
+
| `robot.save_map(name="map") -> bool` | Save the current map. |
|
|
170
|
+
| `robot.load_map(name) -> bool` | Swap the map a *running navigation session* localizes against (nav2 map_server's in-place `/load_map`, auto-reseeding AMCL). Only works while already in navigation mode — use `enter_navigation_mode(name)` to start one. |
|
|
171
|
+
| `robot.delete_map(name) -> bool` | Delete a saved map and its sidecar files. `False` if it doesn't exist or a live navigation session is currently localized against it — `stop_nav_mode()` or switch maps first. |
|
|
172
|
+
| `robot.list_maps() -> list[str]` | Saved map names (the server actually returns richer metadata dicts — `list_maps()` extracts just the name; verified against the real M1 sim 2026-08-04). |
|
|
173
|
+
| `robot.get_map() -> dict` | Latest occupancy grid (decoded from cached `map`). |
|
|
174
|
+
| `robot.get_costmap() -> dict` | Latest costmap (decoded from cached `costmap`), same shape as `get_map()`. |
|
|
175
|
+
|
|
176
|
+
Named locations (semantic waypoints) — **all 🔌 stub in v1**:
|
|
177
|
+
|
|
178
|
+
| Method | Description |
|
|
179
|
+
|---|---|
|
|
180
|
+
| `robot.save_location(name) -> bool` | Save current pose under a name. |
|
|
181
|
+
| `robot.goto_location(name, wait=True) -> bool` | Navigate to a saved location. |
|
|
182
|
+
| `robot.list_locations() -> list[str]` | (`[]` while stubbed.) |
|
|
183
|
+
| `robot.delete_location(name) -> bool` / `robot.delete_all_locations() -> bool` | Manage saved locations. |
|
|
184
|
+
|
|
185
|
+
Grouped access: `robot.nav.*`.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## 5. Arms, grippers & neck
|
|
190
|
+
|
|
191
|
+
Built on `servo_command` (registry camelCase joints → controller groups,
|
|
192
|
+
angles in **degrees** at the API boundary, converted to radians on the wire).
|
|
193
|
+
|
|
194
|
+
| Method | Description |
|
|
195
|
+
|---|---|
|
|
196
|
+
| `robot.set_servos(angles: dict, duration=1.0, wait=True, timeout=None) -> bool` | Set multiple joints, e.g. `{"leftElbow": -30, "neckYaw": 20}`. |
|
|
197
|
+
| `robot.move_left_arm(shoulder, elbow, wait=True, duration=1.0, timeout=None) -> bool` | Left arm shorthand. |
|
|
198
|
+
| `robot.move_right_arm(shoulder, elbow, wait=True, duration=1.0, timeout=None) -> bool` | Right arm shorthand. |
|
|
199
|
+
| `robot.set_grippers(left, right) -> bool` | Both grippers (degrees). |
|
|
200
|
+
| `robot.open_grippers() / close_grippers() -> bool` | Convenience. |
|
|
201
|
+
| `robot.set_neck(yaw) -> bool` / `robot.look_left/right/center() -> bool` | Neck yaw. |
|
|
202
|
+
| `robot.reset_servos() -> bool` | All 18 registry joints to neutral. |
|
|
203
|
+
| `robot.set_single_servo(joint, angle) -> bool` | One joint by name. **🔌 stub** (direct addressing) where no controller group covers it. |
|
|
204
|
+
| `robot.get_servo_angles() -> dict` | From `joint_states` telemetry, keyed by the same **registry camelCase** names (e.g. `"leftElbow"`) commands are sent with — not the raw snake_case URDF names the wire uses underneath. |
|
|
205
|
+
|
|
206
|
+
`ServoID` (§11) enumerates the exact 18 registry keys — the M1's real joint
|
|
207
|
+
set, not the old BLE-hardware set it was originally ported from.
|
|
208
|
+
|
|
209
|
+
> **`wait=True` means the arm actually arrived, not just that the server
|
|
210
|
+
> acked the command** (fixed 2026-08-04 — the
|
|
211
|
+
> test evidence). It polls `get_servo_angles()` until every commanded joint
|
|
212
|
+
> is within `8.6°` (`ArmController.CONVERGENCE_TOLERANCE_DEG`) of its
|
|
213
|
+
> target, or `timeout` elapses — default `max(duration * 3, 5.0)`, padded
|
|
214
|
+
> well above `duration` rather than assuming wall-clock time matches it.
|
|
215
|
+
> `set_servos`/`move_left_arm`/`move_right_arm` accept an explicit
|
|
216
|
+
> `timeout=` to override the default. A joint the server reports as
|
|
217
|
+
> `unknown` (a typo'd key, say) is excluded from the wait rather than
|
|
218
|
+
> spuriously timing out the whole call.
|
|
219
|
+
>
|
|
220
|
+
> **You never need to specify a whole arm — partial calls work correctly**
|
|
221
|
+
> (fixed 2026-08-04): `move_left_arm(shoulder, elbow)`
|
|
222
|
+
> only names 2 of the arm's 7 joints, but the SDK automatically holds the
|
|
223
|
+
> other 5 at their current position so the command isn't silently ignored
|
|
224
|
+
> (the real controller requires a complete joint set per command — an
|
|
225
|
+
> internal detail you don't need to think about; just call the methods
|
|
226
|
+
> normally). One caveat, sim/hardware-side, not an SDK issue: on the current
|
|
227
|
+
> M1 sim, `leftElbow`/`rightElbow` specifically don't respond to position
|
|
228
|
+
> commands (0/60 in an independent stress test, `bonicOS-m1-ros/multiTestReport_stress.md`)
|
|
229
|
+
> — every other joint is 100% reliable.
|
|
230
|
+
>
|
|
231
|
+
> **Calling two arm/servo methods back-to-back without waiting is safe:** a
|
|
232
|
+
> new `servo_command` to a group immediately preempts whatever trajectory
|
|
233
|
+
> was still running, smoothly interpolating from the joint's current
|
|
234
|
+
> position to the new target — it never queues, so there's no risk of a
|
|
235
|
+
> stale command "catching up" later.
|
|
236
|
+
|
|
237
|
+
Grouped access: `robot.arm.*`.
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## 6. Head expression & display — **all 🔌 stub in v1**
|
|
242
|
+
|
|
243
|
+
Carried from the old BLE SDK; no ROS path yet, so these are safe no-ops until the
|
|
244
|
+
robot side lands (PROTOCOL §5.5).
|
|
245
|
+
|
|
246
|
+
| Method | Description |
|
|
247
|
+
|---|---|
|
|
248
|
+
| `robot.set_expression(mode)` | `"normal"/"happy"/"sad"/"angry"/"surprised"/"confused"` (`HeadMode` enum). |
|
|
249
|
+
| `robot.look(pan=None, tilt=None, speed=None)` | Head pan/tilt (prefers the head controller group where it exists). |
|
|
250
|
+
| `robot.set_display_text(text)` | LED-matrix text. |
|
|
251
|
+
| `robot.set_display_color(r, g, b)` | Matrix color. |
|
|
252
|
+
| `robot.set_display_animation(mode)` / `play_display()` / `pause_display()` / `clear_display()` | Matrix animation control. |
|
|
253
|
+
| `robot.set_display_brightness(value)` | Matrix brightness. |
|
|
254
|
+
|
|
255
|
+
Grouped access: `robot.head.*`.
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## 7. Speech
|
|
260
|
+
|
|
261
|
+
One method; the robot decides *where* the audio is produced (Android TTS via
|
|
262
|
+
tablet, or the Pi's own TTS) based on model + config — the caller never picks
|
|
263
|
+
(PROTOCOL §5.6). **🔌 stub on pro until the ESP-relay / on-device TTS path lands;
|
|
264
|
+
fully live on lite** (the Flutter app serves and speaks directly).
|
|
265
|
+
|
|
266
|
+
| Method | Description |
|
|
267
|
+
|---|---|
|
|
268
|
+
| `robot.speak(text, voice=None) -> bool` | Say `text`. Blocks until accepted. |
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## 8. Sensors & telemetry
|
|
273
|
+
|
|
274
|
+
Telemetry is pushed continuously and cached; reads are **non-blocking** and
|
|
275
|
+
return the latest value. Use `wait_for_update()` to pace loops to the real sensor
|
|
276
|
+
rate.
|
|
277
|
+
|
|
278
|
+
| Method | Description |
|
|
279
|
+
|---|---|
|
|
280
|
+
| `robot.get_position() -> dict` | `{x, y, theta}` (map frame). |
|
|
281
|
+
| `robot.get_x() / get_y() / get_heading() -> float` | Individual pose fields (heading in degrees). |
|
|
282
|
+
| `robot.get_battery() -> float` | State of charge (%). |
|
|
283
|
+
| `robot.get_imu() -> dict` | `{ax, ay, az, gx, gy, gz}`. |
|
|
284
|
+
| `robot.get_distance_traveled(start=None) -> float` | Odometry-derived. |
|
|
285
|
+
| `robot.wait_for_update(timeout=1.0) -> bool` | Block until the next telemetry frame. |
|
|
286
|
+
| `robot.wait_for_data(timeout=5.0) -> bool` | Block until first telemetry arrives after connect. |
|
|
287
|
+
| `robot.subscribe(events)` | Narrow the telemetry stream (e.g. `["pose", "battery"]`). |
|
|
288
|
+
|
|
289
|
+
**Recommended loop pattern** (from `bonic-architecture.md` §5 — never spins,
|
|
290
|
+
self-paces to the sensor rate):
|
|
291
|
+
|
|
292
|
+
```python
|
|
293
|
+
while robot.wait_for_update():
|
|
294
|
+
if robot.get_battery() < 15:
|
|
295
|
+
robot.speak("Low battery, returning to base")
|
|
296
|
+
robot.goto_location("charger")
|
|
297
|
+
break
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Grouped access: `robot.sensors.*`.
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## 9. Camera
|
|
305
|
+
|
|
306
|
+
Video is WebRTC under the hood on every transport, but you never have to
|
|
307
|
+
think about that — call `get_camera_frame()`/`get_frame()` and the link
|
|
308
|
+
comes up transparently on first use. Frames are **BGR `numpy` arrays**
|
|
309
|
+
(OpenCV's native layout), same shape on every transport. A multi-camera
|
|
310
|
+
robot (e.g. the M1's face and docking cameras) exposes each by name.
|
|
311
|
+
|
|
312
|
+
| Method | Description |
|
|
313
|
+
|---|---|
|
|
314
|
+
| `robot.list_cameras() -> list[str]` | Camera names from the connect handshake. Available on any transport (informational) — frames still need a video path. |
|
|
315
|
+
| `robot.get_camera_frame(camera=None) -> ndarray \| None` | Latest BGR frame for `camera` (default: the first), or `None` if none has arrived yet. Starts the stream on first call. |
|
|
316
|
+
| `robot.camera.get_frames() -> dict[str, ndarray \| None]` | Latest frame for every camera, keyed by name. |
|
|
317
|
+
| `robot.camera.start(cameras=None)` | Bring the stream up now instead of lazily on first `get_frame()`. Blocks until the link is established (or raises `CameraUnavailable` on timeout/no video path). |
|
|
318
|
+
| `robot.camera.stop()` | Tear down the video path (idempotent). Commands/telemetry are unaffected. |
|
|
319
|
+
|
|
320
|
+
If there is no video path on this connection, camera calls raise `CameraUnavailable` rather than silently returning
|
|
321
|
+
`None` forever.
|
|
322
|
+
|
|
323
|
+
**How it works:** video leaves the robot as WebRTC media tracks, so the SDK
|
|
324
|
+
opens its own `aiortc` peer on first use and hands you decoded frames. You
|
|
325
|
+
never see the peer. It needs the extra deps: `pip install bonicos[camera]`
|
|
326
|
+
(`aiortc`, `numpy`); without them the first camera call raises
|
|
327
|
+
`CameraUnavailable` saying exactly that.
|
|
328
|
+
|
|
329
|
+
Grouped access: `robot.camera.*`.
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## 10. System
|
|
334
|
+
|
|
335
|
+
| Method | Description |
|
|
336
|
+
|---|---|
|
|
337
|
+
| `robot.health() -> dict` | CPU / RAM / temperature / container status. |
|
|
338
|
+
| `robot.restart_base_session(timeout=120.0) -> bool` | Recover a wedged robot: restart the ROS stack *underneath* mapping/navigation (drive, controllers, EKF, sensors, TF) — nav session down, base down, base up, nav session back. **🔒 gated on `session_control`.** Refused while the robot is moving or running a nav goal — cancel/stop first. Slow (cold-start Gazebo alone is ~25s); the long default timeout reflects that, and a WebRTC video peer will drop partway through since the restart takes the camera topics with it. |
|
|
339
|
+
| `robot.get_session_status() -> dict` | Fresh, synchronous `{"base": {...}, "nav": {...}, "health": {...}}` — the full picture behind `system.get_base_session()`/`get_session_health()` in one round trip, without waiting for a push. |
|
|
340
|
+
| `robot.reconfig_wifi(ssid, password) -> bool` | Apply Wi-Fi credentials. |
|
|
341
|
+
| `robot.trigger_update() -> bool` | Pull + restart the robot app. |
|
|
342
|
+
| `robot.ask_llm(prompt, model=None) -> str` | On-device LLM (S/M series). **Display only** — output is never executed as a command. Blocks and returns the full text (tokens stream internally). |
|
|
343
|
+
|
|
344
|
+
Grouped-only (`robot.system.*`, not flattened onto `robot.*` — mirrors
|
|
345
|
+
`get_plan()`/`get_costmap()`):
|
|
346
|
+
|
|
347
|
+
| Method | Description |
|
|
348
|
+
|---|---|
|
|
349
|
+
| `robot.system.get_base_session() -> dict \| None` | Latest cached `base_session` telemetry: `{"running", "owned", "transitioning", "error"}`. `None` before the first frame arrives. |
|
|
350
|
+
| `robot.system.get_session_health() -> dict \| None` | Latest cached `session_health` telemetry: `{"ok", "base", "nav", "issues"}` — `issues` names the mechanism (e.g. `"amcl_not_running"`, `"pose_stale:23s"`), not just a boolean. Pushed only on change, so may still be `None` right after connecting even on a healthy robot; use `get_session_status()` for a guaranteed-fresh read. |
|
|
351
|
+
|
|
352
|
+
Grouped access: `robot.system.*`.
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## 11. Enums & exceptions
|
|
357
|
+
|
|
358
|
+
```python
|
|
359
|
+
from bonicos import HeadMode, ServoID # enums (trimmed to core)
|
|
360
|
+
from bonicos import (
|
|
361
|
+
RobotError, # base
|
|
362
|
+
ConnectionError, # connect/handshake failed
|
|
363
|
+
CommandError, # server returned `error`
|
|
364
|
+
FeatureUnavailable, # gated-off feature for this series
|
|
365
|
+
RobotDisconnected, # link dropped mid-call
|
|
366
|
+
)
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
`FeatureUnavailable` and `RobotDisconnected` surface as **real Python
|
|
370
|
+
exceptions** inside user code (platform requirement) so student programs can
|
|
371
|
+
`try/except` them.
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
## 12. Worked examples
|
|
376
|
+
|
|
377
|
+
**Square patrol with obstacle awareness**
|
|
378
|
+
|
|
379
|
+
```python
|
|
380
|
+
from bonicos import BonicBot
|
|
381
|
+
|
|
382
|
+
with BonicBot("192.168.1.50", robot_id="M1_001") as robot:
|
|
383
|
+
robot.wait_for_data()
|
|
384
|
+
for _ in range(4):
|
|
385
|
+
robot.drive_distance(1.0)
|
|
386
|
+
robot.rotate_angle(90)
|
|
387
|
+
robot.speak("Patrol complete")
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
**Navigate to a saved place, then gesture** (locations/head are 🔌 stub — runs,
|
|
391
|
+
but only navigation moves the robot in v1)
|
|
392
|
+
|
|
393
|
+
```python
|
|
394
|
+
with BonicBot(robot_id="M1_001") as robot: # autodiscovery
|
|
395
|
+
if robot.features["navigation"]:
|
|
396
|
+
robot.goto_location("kitchen") # blocks until arrival
|
|
397
|
+
robot.set_expression("happy") # no-op in v1
|
|
398
|
+
robot.move_right_arm(shoulder=90, elbow=-30)
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
**Running on the robot itself — identical code**
|
|
402
|
+
|
|
403
|
+
The on-robot runner exports `$BONICOS_HOST`/`$BONICOS_ROBOT_ID` for you, so
|
|
404
|
+
the same file you ran from your laptop needs no edits, and
|
|
405
|
+
`wait_for_update()` paces the loop to the real sensor rate:
|
|
406
|
+
|
|
407
|
+
```python
|
|
408
|
+
robot = BonicBot()
|
|
409
|
+
while robot.wait_for_update():
|
|
410
|
+
robot.drive(linear_x=0.2)
|
|
411
|
+
if robot.get_distance_traveled() > 2.0:
|
|
412
|
+
robot.stop(); break
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
---
|
bonicos-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Autobonics Pvt Ltd
|
|
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.
|
bonicos-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bonicos
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: One Python SDK for every BonicBot — transport-agnostic, replaces bonicbot (BLE) and bonicbot-bridge (ROS)
|
|
5
|
+
Author-email: Autobonics Team <admin@autobonics.com>
|
|
6
|
+
Maintainer-email: Autobonics Team <admin@autobonics.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/Autobonics/bonicOS-SDK
|
|
9
|
+
Project-URL: Documentation, https://github.com/Autobonics/bonicOS-SDK/blob/main/API.md
|
|
10
|
+
Project-URL: Repository, https://github.com/Autobonics/bonicOS-SDK.git
|
|
11
|
+
Project-URL: Bug Tracker, https://github.com/Autobonics/bonicOS-SDK/issues
|
|
12
|
+
Keywords: robot,robotics,servo,control,websocket,webrtc,humanoid,bonicbot,bonicos,hardware,automation,education
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Education
|
|
16
|
+
Classifier: Intended Audience :: Science/Research
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Human Machine Interfaces
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Classifier: Topic :: System :: Hardware :: Hardware Drivers
|
|
20
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
21
|
+
Classifier: Programming Language :: Python :: 3
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
26
|
+
Classifier: Operating System :: OS Independent
|
|
27
|
+
Requires-Python: >=3.10
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Requires-Dist: websockets>=12.0
|
|
31
|
+
Provides-Extra: discovery
|
|
32
|
+
Requires-Dist: zeroconf>=0.132; extra == "discovery"
|
|
33
|
+
Provides-Extra: camera
|
|
34
|
+
Requires-Dist: aiortc>=1.6; extra == "camera"
|
|
35
|
+
Requires-Dist: numpy>=1.21; extra == "camera"
|
|
36
|
+
Provides-Extra: dev
|
|
37
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
38
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
39
|
+
Requires-Dist: black; extra == "dev"
|
|
40
|
+
Requires-Dist: flake8; extra == "dev"
|
|
41
|
+
Requires-Dist: mypy; extra == "dev"
|
|
42
|
+
Requires-Dist: numpy<2.1,>=1.21; extra == "dev"
|
|
43
|
+
Dynamic: license-file
|
|
44
|
+
|
|
45
|
+
# bonicos
|
|
46
|
+
|
|
47
|
+
**The Python SDK for BonicBot robots.** Drive the base, move the arms, run
|
|
48
|
+
navigation and mapping, and read live sensor telemetry — from your own laptop
|
|
49
|
+
or from a program running on the robot itself, with the same code either way.
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install bonicos
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from bonicos import BonicBot
|
|
57
|
+
|
|
58
|
+
with BonicBot("192.168.1.50", robot_id="M1_001") as robot:
|
|
59
|
+
robot.move_forward(speed=0.3, duration=2)
|
|
60
|
+
robot.move_left_arm(shoulder_pitch=45, elbow=-30)
|
|
61
|
+
print(robot.get_battery(), "V")
|
|
62
|
+
# motors stopped and the connection closed, even on an exception
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Requires Python 3.11+. Pure Python — the same wheel runs on a laptop, a
|
|
66
|
+
Raspberry Pi, or a Jetson.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Connecting
|
|
71
|
+
|
|
72
|
+
`bonicos` talks to the robot over your local network. You need the robot's
|
|
73
|
+
address and its robot id:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
robot = BonicBot("192.168.1.50", robot_id="M1_001")
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The id must match the robot you're pointing at — it's a guard against driving
|
|
80
|
+
the wrong machine on a network with several robots on it, and the connection is
|
|
81
|
+
refused if it doesn't match.
|
|
82
|
+
|
|
83
|
+
If the environment already knows which robot you mean, a bare `BonicBot()`
|
|
84
|
+
works. That's the case when your program runs **on the robot**, where
|
|
85
|
+
`BONICOS_HOST` and `BONICOS_ROBOT_ID` are set for you — so a script you
|
|
86
|
+
developed on your laptop needs no edits to run on the robot. You can also set
|
|
87
|
+
those variables yourself, or install `bonicos[discovery]` to find a robot by
|
|
88
|
+
mDNS.
|
|
89
|
+
|
|
90
|
+
> **Anyone on the same network can connect.** There is no authentication yet —
|
|
91
|
+
> the robot id is a wrong-robot guard, not a password. Run robots on a network
|
|
92
|
+
> you trust.
|
|
93
|
+
|
|
94
|
+
No robot handy? `BonicBot.simulated()` connects to a fake one instead —
|
|
95
|
+
driving, arms, and telemetry all behave for real, with no network and no
|
|
96
|
+
hardware required.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## What you can do
|
|
101
|
+
|
|
102
|
+
| Area | Examples |
|
|
103
|
+
|---|---|
|
|
104
|
+
| **Motion** | `move_forward`, `move_backward`, `turn_left`, `turn_right`, `stop`, raw `drive` |
|
|
105
|
+
| **Precise motion** | `drive_distance`, `rotate_angle`, `drive_and_rotate`, `draw_square`, queued routines |
|
|
106
|
+
| **Navigation** | `go_to(x, y)`, `navigate_waypoints`, `cancel_goal`, `wait_for_goal`, `get_plan` |
|
|
107
|
+
| **Mapping** | `enter_mapping_mode`, `start_mapping`, `save_map`, `list_maps`, `enter_navigation_mode` |
|
|
108
|
+
| **Arms & grippers** | `move_left_arm`, `move_right_arm`, `set_servos`, `set_gripper`, `set_neck`, `get_servo_angles` |
|
|
109
|
+
| **Sensors** | `get_position`, `get_battery`, `get_imu`, `get_servo_angles`, `wait_for_update` |
|
|
110
|
+
| **Camera** | `get_camera_frame()` → BGR numpy arrays (needs `pip install bonicos[camera]`) |
|
|
111
|
+
| **System** | `speak`, `health`, `ask_llm`, session status and recovery |
|
|
112
|
+
|
|
113
|
+
Full reference with every signature: **[API.md](./API.md)**.
|
|
114
|
+
|
|
115
|
+
Movement calls block until the robot actually gets there — `move_left_arm(...)`
|
|
116
|
+
returns when the joint has converged on its target, not when the command was
|
|
117
|
+
merely acknowledged.
|
|
118
|
+
|
|
119
|
+
### Not yet working on the robot
|
|
120
|
+
|
|
121
|
+
Some commands are accepted and silently do nothing on current robot firmware.
|
|
122
|
+
Your code runs; that actuator just doesn't move. They are marked **🔌 stub** in
|
|
123
|
+
[API.md](./API.md):
|
|
124
|
+
|
|
125
|
+
- Named locations — `save_location`, `goto_location`, `list_locations`,
|
|
126
|
+
`delete_location`, `delete_all_locations`
|
|
127
|
+
- Nav2 lifecycle — `start_navigation`, `stop_navigation`
|
|
128
|
+
- `servo_single`, head expression (`head_mode`, `head_look`), and the LED
|
|
129
|
+
matrix (`display_*`)
|
|
130
|
+
|
|
131
|
+
Everything else in the table above is live. Vision pipelines (face/pose/object
|
|
132
|
+
detection), autonomous exploration, and recorded sequences are not in this
|
|
133
|
+
release.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Safety
|
|
138
|
+
|
|
139
|
+
- Use the context-manager form (`with BonicBot(...) as robot:`) or call
|
|
140
|
+
`robot.close()` in a `finally`. Both stop the robot on the way out.
|
|
141
|
+
- The robot stops its own motors if it stops hearing from you (roughly 400 ms),
|
|
142
|
+
so a crashed script or a dropped connection will not leave it driving. That
|
|
143
|
+
backstop covers driving only — a navigation goal keeps running, so cancel it
|
|
144
|
+
explicitly if you're bailing out.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Installing extras
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
pip install bonicos # driving, arms, navigation, telemetry
|
|
152
|
+
pip install bonicos[camera] # + camera frames (aiortc, numpy)
|
|
153
|
+
pip install bonicos[discovery] # + find a robot by mDNS
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Each extra reports what's missing if you use a feature without it, rather than
|
|
157
|
+
failing with an import error.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Documentation
|
|
162
|
+
|
|
163
|
+
- **[API.md](./API.md)** — every class and method, with blocking behaviour and
|
|
164
|
+
worked examples.
|
|
165
|
+
- **[PROTOCOL.md](./PROTOCOL.md)** — the wire protocol, if you're writing your
|
|
166
|
+
own client or working on the robot side.
|
|
167
|
+
|
|
168
|
+
## License
|
|
169
|
+
|
|
170
|
+
MIT — see [LICENSE](./LICENSE). © Autobonics Pvt Ltd.
|