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.
Files changed (44) hide show
  1. robotrace_sdk-2.0.0/.gitignore +7 -0
  2. robotrace_sdk-2.0.0/LICENSE +21 -0
  3. robotrace_sdk-2.0.0/PKG-INFO +272 -0
  4. robotrace_sdk-2.0.0/README.md +238 -0
  5. robotrace_sdk-2.0.0/pyproject.toml +59 -0
  6. robotrace_sdk-2.0.0/robotrace/__init__.py +97 -0
  7. robotrace_sdk-2.0.0/robotrace/_semconv.py +45 -0
  8. robotrace_sdk-2.0.0/robotrace/client.py +1022 -0
  9. robotrace_sdk-2.0.0/robotrace/commands.py +234 -0
  10. robotrace_sdk-2.0.0/robotrace/component.py +83 -0
  11. robotrace_sdk-2.0.0/robotrace/decorators.py +193 -0
  12. robotrace_sdk-2.0.0/robotrace/fleet.py +240 -0
  13. robotrace_sdk-2.0.0/robotrace/integrations/__init__.py +0 -0
  14. robotrace_sdk-2.0.0/robotrace/integrations/rosp.py +111 -0
  15. robotrace_sdk-2.0.0/robotrace/models.py +309 -0
  16. robotrace_sdk-2.0.0/robotrace/offline_queue.py +360 -0
  17. robotrace_sdk-2.0.0/robotrace/processor.py +173 -0
  18. robotrace_sdk-2.0.0/robotrace/py.typed +0 -0
  19. robotrace_sdk-2.0.0/robotrace/recorder.py +475 -0
  20. robotrace_sdk-2.0.0/robotrace/ros2/__init__.py +41 -0
  21. robotrace_sdk-2.0.0/robotrace/ros2/__main__.py +5 -0
  22. robotrace_sdk-2.0.0/robotrace/ros2/_imports.py +31 -0
  23. robotrace_sdk-2.0.0/robotrace/ros2/_timestamp.py +11 -0
  24. robotrace_sdk-2.0.0/robotrace/ros2/bridge.py +364 -0
  25. robotrace_sdk-2.0.0/robotrace/ros2/converters.py +535 -0
  26. robotrace_sdk-2.0.0/robotrace/sensor.py +259 -0
  27. robotrace_sdk-2.0.0/robotrace/streaming.py +95 -0
  28. robotrace_sdk-2.0.0/robotrace/timelines.py +110 -0
  29. robotrace_sdk-2.0.0/robotrace/types.py +470 -0
  30. robotrace_sdk-2.0.0/tests/__init__.py +0 -0
  31. robotrace_sdk-2.0.0/tests/test_client.py +128 -0
  32. robotrace_sdk-2.0.0/tests/test_commands.py +138 -0
  33. robotrace_sdk-2.0.0/tests/test_component.py +154 -0
  34. robotrace_sdk-2.0.0/tests/test_decorators.py +127 -0
  35. robotrace_sdk-2.0.0/tests/test_fleet.py +216 -0
  36. robotrace_sdk-2.0.0/tests/test_models.py +126 -0
  37. robotrace_sdk-2.0.0/tests/test_offline_queue.py +255 -0
  38. robotrace_sdk-2.0.0/tests/test_processor.py +139 -0
  39. robotrace_sdk-2.0.0/tests/test_recorder.py +426 -0
  40. robotrace_sdk-2.0.0/tests/test_ros2_converters.py +520 -0
  41. robotrace_sdk-2.0.0/tests/test_sensor_pipeline.py +271 -0
  42. robotrace_sdk-2.0.0/tests/test_streaming.py +169 -0
  43. robotrace_sdk-2.0.0/tests/test_timelines.py +95 -0
  44. robotrace_sdk-2.0.0/tests/test_types.py +256 -0
@@ -0,0 +1,7 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .pytest_cache/
4
+ .robotrace/
5
+ dist/
6
+ *.egg-info/
7
+ build/
@@ -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
+