robotrace-sdk 2.0.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.
- robotrace_sdk-2.0.0/.gitignore +7 -0
- robotrace_sdk-2.0.0/LICENSE +21 -0
- robotrace_sdk-2.0.0/PKG-INFO +272 -0
- robotrace_sdk-2.0.0/README.md +238 -0
- robotrace_sdk-2.0.0/pyproject.toml +59 -0
- robotrace_sdk-2.0.0/robotrace/__init__.py +97 -0
- robotrace_sdk-2.0.0/robotrace/_semconv.py +45 -0
- robotrace_sdk-2.0.0/robotrace/client.py +1022 -0
- robotrace_sdk-2.0.0/robotrace/commands.py +234 -0
- robotrace_sdk-2.0.0/robotrace/component.py +83 -0
- robotrace_sdk-2.0.0/robotrace/decorators.py +193 -0
- robotrace_sdk-2.0.0/robotrace/fleet.py +240 -0
- robotrace_sdk-2.0.0/robotrace/integrations/__init__.py +0 -0
- robotrace_sdk-2.0.0/robotrace/integrations/rosp.py +111 -0
- robotrace_sdk-2.0.0/robotrace/models.py +309 -0
- robotrace_sdk-2.0.0/robotrace/offline_queue.py +360 -0
- robotrace_sdk-2.0.0/robotrace/processor.py +173 -0
- robotrace_sdk-2.0.0/robotrace/py.typed +0 -0
- robotrace_sdk-2.0.0/robotrace/recorder.py +475 -0
- robotrace_sdk-2.0.0/robotrace/ros2/__init__.py +41 -0
- robotrace_sdk-2.0.0/robotrace/ros2/__main__.py +5 -0
- robotrace_sdk-2.0.0/robotrace/ros2/_imports.py +31 -0
- robotrace_sdk-2.0.0/robotrace/ros2/_timestamp.py +11 -0
- robotrace_sdk-2.0.0/robotrace/ros2/bridge.py +364 -0
- robotrace_sdk-2.0.0/robotrace/ros2/converters.py +535 -0
- robotrace_sdk-2.0.0/robotrace/sensor.py +259 -0
- robotrace_sdk-2.0.0/robotrace/streaming.py +95 -0
- robotrace_sdk-2.0.0/robotrace/timelines.py +110 -0
- robotrace_sdk-2.0.0/robotrace/types.py +470 -0
- robotrace_sdk-2.0.0/tests/__init__.py +0 -0
- robotrace_sdk-2.0.0/tests/test_client.py +128 -0
- robotrace_sdk-2.0.0/tests/test_commands.py +138 -0
- robotrace_sdk-2.0.0/tests/test_component.py +154 -0
- robotrace_sdk-2.0.0/tests/test_decorators.py +127 -0
- robotrace_sdk-2.0.0/tests/test_fleet.py +216 -0
- robotrace_sdk-2.0.0/tests/test_models.py +126 -0
- robotrace_sdk-2.0.0/tests/test_offline_queue.py +255 -0
- robotrace_sdk-2.0.0/tests/test_processor.py +139 -0
- robotrace_sdk-2.0.0/tests/test_recorder.py +426 -0
- robotrace_sdk-2.0.0/tests/test_ros2_converters.py +520 -0
- robotrace_sdk-2.0.0/tests/test_sensor_pipeline.py +271 -0
- robotrace_sdk-2.0.0/tests/test_streaming.py +169 -0
- robotrace_sdk-2.0.0/tests/test_timelines.py +95 -0
- robotrace_sdk-2.0.0/tests/test_types.py +256 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 RoboSwarm
|
|
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.
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: robotrace-sdk
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: OTel-native Python SDK for robot observability — mission tracing, sensor telemetry, and evaluation
|
|
5
|
+
Project-URL: Homepage, https://robotrace.io
|
|
6
|
+
Project-URL: Documentation, https://docs.robotrace.io
|
|
7
|
+
Project-URL: Source, https://github.com/roboswarm/robotrace-sdk
|
|
8
|
+
Author-email: RoboSwarm Team <sdk@robotrace.io>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: observability,opentelemetry,robotics,rosp,telemetry,tracing
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: httpx>=0.27
|
|
22
|
+
Requires-Dist: opentelemetry-api>=1.27
|
|
23
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.27
|
|
24
|
+
Requires-Dist: opentelemetry-sdk>=1.27
|
|
25
|
+
Provides-Extra: commands
|
|
26
|
+
Requires-Dist: websockets>=12.0; extra == 'commands'
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
30
|
+
Provides-Extra: mcap
|
|
31
|
+
Requires-Dist: mcap<2,>=1.1; extra == 'mcap'
|
|
32
|
+
Requires-Dist: zstandard>=0.19; extra == 'mcap'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# RoboTrace SDK
|
|
36
|
+
|
|
37
|
+
OTel-native Python SDK for robot observability. Mission tracing, sensor telemetry, evaluation scoring -- think Langfuse, but for robots.
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install robotrace-sdk
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Optional extras:
|
|
46
|
+
```bash
|
|
47
|
+
pip install robotrace-sdk[mcap] # MCAP recording
|
|
48
|
+
pip install robotrace-sdk[commands] # Inbound command channel (WebSocket)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Quick Start
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from robotrace import RoboTrace, Scalar, Pose3D, Battery
|
|
55
|
+
|
|
56
|
+
rt = RoboTrace(
|
|
57
|
+
host="https://your-robotrace-server.com",
|
|
58
|
+
public_key="rt-pk-xxx",
|
|
59
|
+
secret_key="rt-sk-xxx",
|
|
60
|
+
device_id="amr-001",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Log sensor telemetry
|
|
64
|
+
rt.log("robot/pose", Pose3D(x=1.0, y=2.0, z=0.0))
|
|
65
|
+
rt.log("sensors/battery", Battery(voltage=12.6, soc=0.875, current=-2.1))
|
|
66
|
+
|
|
67
|
+
# Track missions
|
|
68
|
+
with rt.mission("deliver_pallet", tags=["zone-B"]) as m:
|
|
69
|
+
with m.phase("navigate"):
|
|
70
|
+
rt.log("robot/pose", Pose3D(x=5.0, y=3.0, z=0.0))
|
|
71
|
+
m.score("success", True)
|
|
72
|
+
|
|
73
|
+
rt.shutdown()
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## ROS2 Integration
|
|
77
|
+
|
|
78
|
+
Connect to any ROS2 robot with zero code changes (bridge) or 3 lines (converters):
|
|
79
|
+
|
|
80
|
+
### Converters (in your code)
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from robotrace.ros2 import from_odometry, from_laser_scan, from_battery_state
|
|
84
|
+
|
|
85
|
+
# In your ROS2 callback:
|
|
86
|
+
rt.log("robot/pose", from_odometry(odom_msg))
|
|
87
|
+
rt.log("sensors/lidar", from_laser_scan(scan_msg))
|
|
88
|
+
rt.log("sensors/battery", from_battery_state(battery_msg))
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Bridge (zero code changes)
|
|
92
|
+
|
|
93
|
+
```yaml
|
|
94
|
+
# bridge.yaml
|
|
95
|
+
robotrace:
|
|
96
|
+
host: https://your-server.com
|
|
97
|
+
public_key: rt-pk-xxx
|
|
98
|
+
secret_key: rt-sk-xxx
|
|
99
|
+
device_id: my-robot
|
|
100
|
+
topics:
|
|
101
|
+
/scan: {stream: sensors/lidar, type: LaserScan}
|
|
102
|
+
/odom: {stream: robot/pose, type: Odometry}
|
|
103
|
+
/battery_state: {stream: sensors/battery, type: BatteryState}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
python -m robotrace.ros2 --config bridge.yaml
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
16 converters included: LaserScan, Imu, BatteryState, Odometry, Twist, PoseStamped, NavSatFix, JointState, Image, CompressedImage, PointCloud2, Path, DiagnosticArray, Temperature, Range.
|
|
111
|
+
|
|
112
|
+
## Stream Configuration
|
|
113
|
+
|
|
114
|
+
Tell the platform what your data means (units, ranges, thresholds):
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
rt.configure_stream("sensors/battery", {
|
|
118
|
+
"display_name": "Main Battery",
|
|
119
|
+
"data_type": "battery",
|
|
120
|
+
"fields": {
|
|
121
|
+
"soc": {"unit": "ratio", "scale": 100, "display_unit": "%"},
|
|
122
|
+
"voltage": {"unit": "V", "range": [10.0, 14.8]},
|
|
123
|
+
"temperature": {"unit": "C", "warning_above": 60},
|
|
124
|
+
},
|
|
125
|
+
"rate_hz": 1,
|
|
126
|
+
})
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Decorator Pattern
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from robotrace import mission, phase
|
|
133
|
+
|
|
134
|
+
@mission(name="deliver_pallet")
|
|
135
|
+
def deliver(destination):
|
|
136
|
+
navigate_to(destination)
|
|
137
|
+
pick_up()
|
|
138
|
+
return {"status": "delivered"}
|
|
139
|
+
|
|
140
|
+
@phase()
|
|
141
|
+
def navigate_to(destination):
|
|
142
|
+
rt.log("robot/pose", Pose3D(x=1.0, y=2.0, z=0.0))
|
|
143
|
+
|
|
144
|
+
# Works with async too
|
|
145
|
+
@mission()
|
|
146
|
+
async def async_delivery(destination):
|
|
147
|
+
await navigate_async(destination)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Context Manager Pattern
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
with rt.mission("pick_and_place", tags=["priority-high"]) as m:
|
|
154
|
+
with m.phase("navigate") as nav:
|
|
155
|
+
nav.log("robot/pose", Pose3D(x=1.0, y=2.0, z=0.0))
|
|
156
|
+
nav.event("obstacle_detected", {"distance_m": 0.5})
|
|
157
|
+
with m.phase("pick") as pick:
|
|
158
|
+
pick.decision("grasp_planner", model="graspnet-v2",
|
|
159
|
+
input={"target": [0, 0, 0]}, output={"confidence": 0.95})
|
|
160
|
+
m.score("cycle_time_s", 14.2)
|
|
161
|
+
m.score("success", True)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Data Types
|
|
165
|
+
|
|
166
|
+
20 typed sensor data types:
|
|
167
|
+
|
|
168
|
+
| Type | Description |
|
|
169
|
+
|------|-------------|
|
|
170
|
+
| `Scalar(value)` | Single numeric value |
|
|
171
|
+
| `Vector3(x, y, z)` | 3D vector |
|
|
172
|
+
| `VectorN(values)` | N-dimensional vector |
|
|
173
|
+
| `Pose3D(x, y, z, qx, qy, qz, qw)` | 6DOF pose with quaternion |
|
|
174
|
+
| `Transform3D(translation, rotation)` | Coordinate transform |
|
|
175
|
+
| `PointCloud(points, colors)` | Nx3 point cloud |
|
|
176
|
+
| `Image(data, format)` | Compressed image (JPEG/PNG) |
|
|
177
|
+
| `DepthImage(data)` | Depth map |
|
|
178
|
+
| `Video(data, format)` | Video clip |
|
|
179
|
+
| `LaserScan(ranges, angle_min, angle_max, angle_increment)` | 2D LiDAR scan |
|
|
180
|
+
| `JointState(names, positions, velocities, efforts)` | Multi-DOF joint state |
|
|
181
|
+
| `NumericSet(values)` | Named numeric values |
|
|
182
|
+
| `GeoLocation(latitude, longitude, altitude, heading, speed)` | GPS/GNSS position |
|
|
183
|
+
| `Path(points, frame_id)` | Trajectory waypoints |
|
|
184
|
+
| `Twist(linear_x/y/z, angular_x/y/z)` | 6DOF velocity |
|
|
185
|
+
| `BoundingBox2D(x, y, w, h, label)` | 2D detection |
|
|
186
|
+
| `BoundingBox3D(center, size, label)` | 3D detection |
|
|
187
|
+
| `Log(message, level)` | Text log message |
|
|
188
|
+
| `Battery(voltage, current, soc, health, temperature)` | Battery state |
|
|
189
|
+
| `Bitset(value, width, labels)` | Hardware status register |
|
|
190
|
+
|
|
191
|
+
## Fleet Gateway
|
|
192
|
+
|
|
193
|
+
Manage telemetry for multiple robots from one process:
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
from robotrace import RoboTraceFleet
|
|
197
|
+
|
|
198
|
+
fleet = RoboTraceFleet(
|
|
199
|
+
host="https://your-server.com",
|
|
200
|
+
public_key="rt-pk-xxx",
|
|
201
|
+
secret_key="rt-sk-xxx",
|
|
202
|
+
device_ids=["amr-001", "amr-002", "amr-003"],
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
with fleet.mission("amr-001", "deliver_pallet") as m:
|
|
206
|
+
fleet.log("amr-001", "sensors/battery", Battery(voltage=12.4))
|
|
207
|
+
m.score("success", True)
|
|
208
|
+
|
|
209
|
+
fleet.broadcast_log("fleet/heartbeat", Scalar(1.0))
|
|
210
|
+
fleet.shutdown_all()
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Inbound Commands
|
|
214
|
+
|
|
215
|
+
Receive commands from the server (requires `pip install robotrace-sdk[commands]`):
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
cmd = rt.command_channel()
|
|
219
|
+
|
|
220
|
+
@cmd.on("e_stop")
|
|
221
|
+
def handle_estop(payload):
|
|
222
|
+
robot.emergency_stop()
|
|
223
|
+
|
|
224
|
+
@cmd.on("set_speed")
|
|
225
|
+
def handle_speed(payload):
|
|
226
|
+
robot.set_max_speed(payload["max_speed"])
|
|
227
|
+
|
|
228
|
+
cmd.start()
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## MCAP Recording
|
|
232
|
+
|
|
233
|
+
Dual-output: telemetry goes to both the server AND a local MCAP file (requires `pip install robotrace-sdk[mcap]`):
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
rt.start_recording("mission.mcap")
|
|
237
|
+
# All subsequent log() calls also write to the MCAP file
|
|
238
|
+
rt.stop_recording()
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Pipeline Health
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
health = rt.health()
|
|
245
|
+
# {'buffered_count': 5, 'offline_count': 0, 'stats': {...}, 'is_recording': False}
|
|
246
|
+
|
|
247
|
+
rt = RoboTrace(..., on_error=lambda ctx, exc: print(f"Error in {ctx}: {exc}"))
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Environment Variables
|
|
251
|
+
|
|
252
|
+
| Variable | Default | Description |
|
|
253
|
+
|----------|---------|-------------|
|
|
254
|
+
| `ROBOTRACE_HOST` | (none) | Server URL (fallback if `host` not set) |
|
|
255
|
+
| `ROBOTRACE_DEBUG` | `false` | Enable debug logging to stderr |
|
|
256
|
+
| `ROBOTRACE_LOG_LEVEL` | `WARNING` | Log level: DEBUG, INFO, WARNING, ERROR |
|
|
257
|
+
| `ROBOTRACE_LOG_FILE` | (none) | Write SDK logs to this file |
|
|
258
|
+
| `ROBOTRACE_DATA_DIR` | `~/.robotrace` | Directory for offline queue database |
|
|
259
|
+
| `ROBOTRACE_OFFLINE_MAX_MB` | `100` | Max offline queue size in MB |
|
|
260
|
+
| `ROBOTRACE_OFFLINE_MAX_HOURS` | `72` | Max offline queue age in hours |
|
|
261
|
+
|
|
262
|
+
## Development
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
pip install -e ".[dev]"
|
|
266
|
+
pytest # 242 tests
|
|
267
|
+
pytest tests/test_ros2_converters.py # ROS2 converter tests only
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## License
|
|
271
|
+
|
|
272
|
+
MIT
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# RoboTrace SDK
|
|
2
|
+
|
|
3
|
+
OTel-native Python SDK for robot observability. Mission tracing, sensor telemetry, evaluation scoring -- think Langfuse, but for robots.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install robotrace-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Optional extras:
|
|
12
|
+
```bash
|
|
13
|
+
pip install robotrace-sdk[mcap] # MCAP recording
|
|
14
|
+
pip install robotrace-sdk[commands] # Inbound command channel (WebSocket)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from robotrace import RoboTrace, Scalar, Pose3D, Battery
|
|
21
|
+
|
|
22
|
+
rt = RoboTrace(
|
|
23
|
+
host="https://your-robotrace-server.com",
|
|
24
|
+
public_key="rt-pk-xxx",
|
|
25
|
+
secret_key="rt-sk-xxx",
|
|
26
|
+
device_id="amr-001",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Log sensor telemetry
|
|
30
|
+
rt.log("robot/pose", Pose3D(x=1.0, y=2.0, z=0.0))
|
|
31
|
+
rt.log("sensors/battery", Battery(voltage=12.6, soc=0.875, current=-2.1))
|
|
32
|
+
|
|
33
|
+
# Track missions
|
|
34
|
+
with rt.mission("deliver_pallet", tags=["zone-B"]) as m:
|
|
35
|
+
with m.phase("navigate"):
|
|
36
|
+
rt.log("robot/pose", Pose3D(x=5.0, y=3.0, z=0.0))
|
|
37
|
+
m.score("success", True)
|
|
38
|
+
|
|
39
|
+
rt.shutdown()
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## ROS2 Integration
|
|
43
|
+
|
|
44
|
+
Connect to any ROS2 robot with zero code changes (bridge) or 3 lines (converters):
|
|
45
|
+
|
|
46
|
+
### Converters (in your code)
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from robotrace.ros2 import from_odometry, from_laser_scan, from_battery_state
|
|
50
|
+
|
|
51
|
+
# In your ROS2 callback:
|
|
52
|
+
rt.log("robot/pose", from_odometry(odom_msg))
|
|
53
|
+
rt.log("sensors/lidar", from_laser_scan(scan_msg))
|
|
54
|
+
rt.log("sensors/battery", from_battery_state(battery_msg))
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Bridge (zero code changes)
|
|
58
|
+
|
|
59
|
+
```yaml
|
|
60
|
+
# bridge.yaml
|
|
61
|
+
robotrace:
|
|
62
|
+
host: https://your-server.com
|
|
63
|
+
public_key: rt-pk-xxx
|
|
64
|
+
secret_key: rt-sk-xxx
|
|
65
|
+
device_id: my-robot
|
|
66
|
+
topics:
|
|
67
|
+
/scan: {stream: sensors/lidar, type: LaserScan}
|
|
68
|
+
/odom: {stream: robot/pose, type: Odometry}
|
|
69
|
+
/battery_state: {stream: sensors/battery, type: BatteryState}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
python -m robotrace.ros2 --config bridge.yaml
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
16 converters included: LaserScan, Imu, BatteryState, Odometry, Twist, PoseStamped, NavSatFix, JointState, Image, CompressedImage, PointCloud2, Path, DiagnosticArray, Temperature, Range.
|
|
77
|
+
|
|
78
|
+
## Stream Configuration
|
|
79
|
+
|
|
80
|
+
Tell the platform what your data means (units, ranges, thresholds):
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
rt.configure_stream("sensors/battery", {
|
|
84
|
+
"display_name": "Main Battery",
|
|
85
|
+
"data_type": "battery",
|
|
86
|
+
"fields": {
|
|
87
|
+
"soc": {"unit": "ratio", "scale": 100, "display_unit": "%"},
|
|
88
|
+
"voltage": {"unit": "V", "range": [10.0, 14.8]},
|
|
89
|
+
"temperature": {"unit": "C", "warning_above": 60},
|
|
90
|
+
},
|
|
91
|
+
"rate_hz": 1,
|
|
92
|
+
})
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Decorator Pattern
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from robotrace import mission, phase
|
|
99
|
+
|
|
100
|
+
@mission(name="deliver_pallet")
|
|
101
|
+
def deliver(destination):
|
|
102
|
+
navigate_to(destination)
|
|
103
|
+
pick_up()
|
|
104
|
+
return {"status": "delivered"}
|
|
105
|
+
|
|
106
|
+
@phase()
|
|
107
|
+
def navigate_to(destination):
|
|
108
|
+
rt.log("robot/pose", Pose3D(x=1.0, y=2.0, z=0.0))
|
|
109
|
+
|
|
110
|
+
# Works with async too
|
|
111
|
+
@mission()
|
|
112
|
+
async def async_delivery(destination):
|
|
113
|
+
await navigate_async(destination)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Context Manager Pattern
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
with rt.mission("pick_and_place", tags=["priority-high"]) as m:
|
|
120
|
+
with m.phase("navigate") as nav:
|
|
121
|
+
nav.log("robot/pose", Pose3D(x=1.0, y=2.0, z=0.0))
|
|
122
|
+
nav.event("obstacle_detected", {"distance_m": 0.5})
|
|
123
|
+
with m.phase("pick") as pick:
|
|
124
|
+
pick.decision("grasp_planner", model="graspnet-v2",
|
|
125
|
+
input={"target": [0, 0, 0]}, output={"confidence": 0.95})
|
|
126
|
+
m.score("cycle_time_s", 14.2)
|
|
127
|
+
m.score("success", True)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Data Types
|
|
131
|
+
|
|
132
|
+
20 typed sensor data types:
|
|
133
|
+
|
|
134
|
+
| Type | Description |
|
|
135
|
+
|------|-------------|
|
|
136
|
+
| `Scalar(value)` | Single numeric value |
|
|
137
|
+
| `Vector3(x, y, z)` | 3D vector |
|
|
138
|
+
| `VectorN(values)` | N-dimensional vector |
|
|
139
|
+
| `Pose3D(x, y, z, qx, qy, qz, qw)` | 6DOF pose with quaternion |
|
|
140
|
+
| `Transform3D(translation, rotation)` | Coordinate transform |
|
|
141
|
+
| `PointCloud(points, colors)` | Nx3 point cloud |
|
|
142
|
+
| `Image(data, format)` | Compressed image (JPEG/PNG) |
|
|
143
|
+
| `DepthImage(data)` | Depth map |
|
|
144
|
+
| `Video(data, format)` | Video clip |
|
|
145
|
+
| `LaserScan(ranges, angle_min, angle_max, angle_increment)` | 2D LiDAR scan |
|
|
146
|
+
| `JointState(names, positions, velocities, efforts)` | Multi-DOF joint state |
|
|
147
|
+
| `NumericSet(values)` | Named numeric values |
|
|
148
|
+
| `GeoLocation(latitude, longitude, altitude, heading, speed)` | GPS/GNSS position |
|
|
149
|
+
| `Path(points, frame_id)` | Trajectory waypoints |
|
|
150
|
+
| `Twist(linear_x/y/z, angular_x/y/z)` | 6DOF velocity |
|
|
151
|
+
| `BoundingBox2D(x, y, w, h, label)` | 2D detection |
|
|
152
|
+
| `BoundingBox3D(center, size, label)` | 3D detection |
|
|
153
|
+
| `Log(message, level)` | Text log message |
|
|
154
|
+
| `Battery(voltage, current, soc, health, temperature)` | Battery state |
|
|
155
|
+
| `Bitset(value, width, labels)` | Hardware status register |
|
|
156
|
+
|
|
157
|
+
## Fleet Gateway
|
|
158
|
+
|
|
159
|
+
Manage telemetry for multiple robots from one process:
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
from robotrace import RoboTraceFleet
|
|
163
|
+
|
|
164
|
+
fleet = RoboTraceFleet(
|
|
165
|
+
host="https://your-server.com",
|
|
166
|
+
public_key="rt-pk-xxx",
|
|
167
|
+
secret_key="rt-sk-xxx",
|
|
168
|
+
device_ids=["amr-001", "amr-002", "amr-003"],
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
with fleet.mission("amr-001", "deliver_pallet") as m:
|
|
172
|
+
fleet.log("amr-001", "sensors/battery", Battery(voltage=12.4))
|
|
173
|
+
m.score("success", True)
|
|
174
|
+
|
|
175
|
+
fleet.broadcast_log("fleet/heartbeat", Scalar(1.0))
|
|
176
|
+
fleet.shutdown_all()
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Inbound Commands
|
|
180
|
+
|
|
181
|
+
Receive commands from the server (requires `pip install robotrace-sdk[commands]`):
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
cmd = rt.command_channel()
|
|
185
|
+
|
|
186
|
+
@cmd.on("e_stop")
|
|
187
|
+
def handle_estop(payload):
|
|
188
|
+
robot.emergency_stop()
|
|
189
|
+
|
|
190
|
+
@cmd.on("set_speed")
|
|
191
|
+
def handle_speed(payload):
|
|
192
|
+
robot.set_max_speed(payload["max_speed"])
|
|
193
|
+
|
|
194
|
+
cmd.start()
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## MCAP Recording
|
|
198
|
+
|
|
199
|
+
Dual-output: telemetry goes to both the server AND a local MCAP file (requires `pip install robotrace-sdk[mcap]`):
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
rt.start_recording("mission.mcap")
|
|
203
|
+
# All subsequent log() calls also write to the MCAP file
|
|
204
|
+
rt.stop_recording()
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Pipeline Health
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
health = rt.health()
|
|
211
|
+
# {'buffered_count': 5, 'offline_count': 0, 'stats': {...}, 'is_recording': False}
|
|
212
|
+
|
|
213
|
+
rt = RoboTrace(..., on_error=lambda ctx, exc: print(f"Error in {ctx}: {exc}"))
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Environment Variables
|
|
217
|
+
|
|
218
|
+
| Variable | Default | Description |
|
|
219
|
+
|----------|---------|-------------|
|
|
220
|
+
| `ROBOTRACE_HOST` | (none) | Server URL (fallback if `host` not set) |
|
|
221
|
+
| `ROBOTRACE_DEBUG` | `false` | Enable debug logging to stderr |
|
|
222
|
+
| `ROBOTRACE_LOG_LEVEL` | `WARNING` | Log level: DEBUG, INFO, WARNING, ERROR |
|
|
223
|
+
| `ROBOTRACE_LOG_FILE` | (none) | Write SDK logs to this file |
|
|
224
|
+
| `ROBOTRACE_DATA_DIR` | `~/.robotrace` | Directory for offline queue database |
|
|
225
|
+
| `ROBOTRACE_OFFLINE_MAX_MB` | `100` | Max offline queue size in MB |
|
|
226
|
+
| `ROBOTRACE_OFFLINE_MAX_HOURS` | `72` | Max offline queue age in hours |
|
|
227
|
+
|
|
228
|
+
## Development
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
pip install -e ".[dev]"
|
|
232
|
+
pytest # 242 tests
|
|
233
|
+
pytest tests/test_ros2_converters.py # ROS2 converter tests only
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## License
|
|
237
|
+
|
|
238
|
+
MIT
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "robotrace-sdk"
|
|
7
|
+
version = "2.0.0"
|
|
8
|
+
description = "OTel-native Python SDK for robot observability — mission tracing, sensor telemetry, and evaluation"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [{ name = "RoboSwarm Team", email = "sdk@robotrace.io" }]
|
|
13
|
+
keywords = ["robotics", "telemetry", "tracing", "opentelemetry", "observability", "rosp"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Topic :: Scientific/Engineering",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
dependencies = [
|
|
26
|
+
"opentelemetry-api>=1.27",
|
|
27
|
+
"opentelemetry-sdk>=1.27",
|
|
28
|
+
"opentelemetry-exporter-otlp-proto-http>=1.27",
|
|
29
|
+
"httpx>=0.27",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
mcap = [
|
|
34
|
+
"mcap>=1.1,<2",
|
|
35
|
+
"zstandard>=0.19",
|
|
36
|
+
]
|
|
37
|
+
commands = [
|
|
38
|
+
"websockets>=12.0",
|
|
39
|
+
]
|
|
40
|
+
dev = [
|
|
41
|
+
"pytest>=8.0",
|
|
42
|
+
"pytest-asyncio>=0.23",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[project.urls]
|
|
46
|
+
Homepage = "https://robotrace.io"
|
|
47
|
+
Documentation = "https://docs.robotrace.io"
|
|
48
|
+
Source = "https://github.com/roboswarm/robotrace-sdk"
|
|
49
|
+
|
|
50
|
+
[tool.hatch.build.targets.sdist]
|
|
51
|
+
exclude = [".robotrace/", ".pytest_cache/", "__pycache__/"]
|
|
52
|
+
|
|
53
|
+
[tool.hatch.build.targets.wheel]
|
|
54
|
+
packages = ["robotrace"]
|
|
55
|
+
exclude = [".robotrace/", ".pytest_cache/", "__pycache__/"]
|
|
56
|
+
|
|
57
|
+
[tool.pytest.ini_options]
|
|
58
|
+
testpaths = ["tests"]
|
|
59
|
+
asyncio_mode = "auto"
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""RoboTrace SDK v2 — OTel-native robot observability.
|
|
2
|
+
|
|
3
|
+
Quick start::
|
|
4
|
+
|
|
5
|
+
from robotrace import RoboTrace, mission, phase, Scalar, Pose3D
|
|
6
|
+
|
|
7
|
+
rt = RoboTrace(
|
|
8
|
+
host="http://localhost:8080",
|
|
9
|
+
public_key="rt-pk-xxx",
|
|
10
|
+
secret_key="rt-sk-xxx",
|
|
11
|
+
device_id="urn:rosp:device:robotis:turtlebot3:001",
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
with rt.mission("deliver_pallet", tags=["zone-B"]) as m:
|
|
15
|
+
with m.phase("navigate") as nav:
|
|
16
|
+
nav.log("robot/pose", Pose3D(x=1.0, y=2.0, z=0.0))
|
|
17
|
+
nav.log("sensors/battery", Scalar(87.5))
|
|
18
|
+
m.score("success", True)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import logging as _logging
|
|
22
|
+
|
|
23
|
+
# Python library best practice: NullHandler prevents "No handler found" warnings
|
|
24
|
+
# when the application doesn't configure logging. Users configure their own handlers.
|
|
25
|
+
_logging.getLogger("robotrace").addHandler(_logging.NullHandler())
|
|
26
|
+
|
|
27
|
+
# Single version source — read from installed package metadata, fallback to hardcoded.
|
|
28
|
+
# Must be defined before submodule imports (client.py and decorators.py reference it).
|
|
29
|
+
try:
|
|
30
|
+
from importlib.metadata import version as _pkg_version
|
|
31
|
+
__version__ = _pkg_version("robotrace-sdk")
|
|
32
|
+
except Exception:
|
|
33
|
+
__version__ = "2.0.0"
|
|
34
|
+
|
|
35
|
+
from .client import RoboTrace
|
|
36
|
+
from .commands import CommandChannel
|
|
37
|
+
from .component import Component
|
|
38
|
+
from .fleet import RoboTraceFleet
|
|
39
|
+
from .decorators import mission, phase
|
|
40
|
+
from .recorder import McapRecorder, mcap_available
|
|
41
|
+
from .streaming import StreamingCamera
|
|
42
|
+
from .types import (
|
|
43
|
+
Battery,
|
|
44
|
+
Bitset,
|
|
45
|
+
BoundingBox2D,
|
|
46
|
+
BoundingBox3D,
|
|
47
|
+
DepthImage,
|
|
48
|
+
GeoLocation,
|
|
49
|
+
Image,
|
|
50
|
+
JointState,
|
|
51
|
+
LaserScan,
|
|
52
|
+
Log,
|
|
53
|
+
NumericSet,
|
|
54
|
+
Path,
|
|
55
|
+
PointCloud,
|
|
56
|
+
Pose3D,
|
|
57
|
+
Scalar,
|
|
58
|
+
Transform3D,
|
|
59
|
+
Twist,
|
|
60
|
+
Vector3,
|
|
61
|
+
VectorN,
|
|
62
|
+
Video,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
__all__ = [
|
|
66
|
+
"RoboTrace",
|
|
67
|
+
"RoboTraceFleet",
|
|
68
|
+
"CommandChannel",
|
|
69
|
+
"Component",
|
|
70
|
+
"mission",
|
|
71
|
+
"phase",
|
|
72
|
+
"McapRecorder",
|
|
73
|
+
"mcap_available",
|
|
74
|
+
"StreamingCamera",
|
|
75
|
+
# Timeline values are set via rt.set_time() / rt.reset_time() on the RoboTrace instance
|
|
76
|
+
"Scalar",
|
|
77
|
+
"Vector3",
|
|
78
|
+
"VectorN",
|
|
79
|
+
"Pose3D",
|
|
80
|
+
"Transform3D",
|
|
81
|
+
"PointCloud",
|
|
82
|
+
"Image",
|
|
83
|
+
"DepthImage",
|
|
84
|
+
"LaserScan",
|
|
85
|
+
"JointState",
|
|
86
|
+
"NumericSet",
|
|
87
|
+
"GeoLocation",
|
|
88
|
+
"Path",
|
|
89
|
+
"Twist",
|
|
90
|
+
"BoundingBox2D",
|
|
91
|
+
"BoundingBox3D",
|
|
92
|
+
"Video",
|
|
93
|
+
"Log",
|
|
94
|
+
"Battery",
|
|
95
|
+
"Bitset",
|
|
96
|
+
]
|
|
97
|
+
|