topicforge 0.6.3__tar.gz → 0.6.4__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.
- {topicforge-0.6.3 → topicforge-0.6.4}/CHANGELOG.md +36 -1
- {topicforge-0.6.3 → topicforge-0.6.4}/PKG-INFO +7 -8
- {topicforge-0.6.3 → topicforge-0.6.4}/README.md +5 -5
- {topicforge-0.6.3 → topicforge-0.6.4}/docs/CLIENTS.md +20 -16
- {topicforge-0.6.3 → topicforge-0.6.4}/docs/TESTING.md +4 -4
- {topicforge-0.6.3 → topicforge-0.6.4}/docs/TROUBLESHOOTING.md +1 -1
- {topicforge-0.6.3 → topicforge-0.6.4}/docs/TUTORIEL.md +1 -1
- {topicforge-0.6.3 → topicforge-0.6.4}/plugin/README.md +7 -1
- {topicforge-0.6.3 → topicforge-0.6.4}/plugin/pyproject.toml +2 -2
- {topicforge-0.6.3 → topicforge-0.6.4}/pyproject.toml +10 -13
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/__init__.py +1 -1
- topicforge-0.6.4/src/topicforge/adapters/common/stamps.py +58 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/adapters/ros2_live/echo_parser.py +8 -16
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/adapters/ros2_mock/fixtures.py +7 -4
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/models/schemas.py +9 -5
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/server/app.py +15 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/services/bag_service.py +7 -17
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/tools/handlers.py +7 -4
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_bag_omnisim_humble.py +26 -1
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_mock_adapter.py +4 -3
- topicforge-0.6.4/tests/test_stamps.py +107 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_version_consistency.py +24 -1
- {topicforge-0.6.3 → topicforge-0.6.4}/.gitignore +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/LICENSE +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/docs/DDS_QUICKSTART.md +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/docs/VALIDATION.md +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/docs/dds-interop-matrix.md +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/examples/README.md +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/examples/dds/00_hello_pub_sub/README.md +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/examples/dds/01_who_is_on_the_bus/README.md +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/examples/dds/02_why_cant_they_talk/README.md +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/examples/dds/03_a_node_crashed/README.md +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/examples/dds/04_late_joiner_misses_data/README.md +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/examples/dds/05_reliability_in_code/README.md +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/examples/dds/06_durability_late_joiner_in_code/README.md +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/examples/dds/07_deadline_in_code/README.md +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/examples/dds/08_crash_seen_from_inside/README.md +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/examples/dds/10_lidar_silent_after_driver_swap/README.md +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/examples/dds/11_who_talks_to_whom/README.md +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/examples/dds/12_safety_monitor_dropout/README.md +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/examples/dds/13_deadline_not_offered/README.md +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/examples/dds/14_restart_loop/README.md +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/examples/dds/README.md +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/plugin/LICENSE +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/scripts/agent_eval/README.md +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/scripts/integration/README.md +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/scripts/integration/publishers/cyclone_c/README.md +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/scripts/integration/publishers/cyclone_cpp/README.md +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/scripts/integration/publishers/cyclone_rust/README.md +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/scripts/integration/publishers/dust_py/README.md +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/scripts/integration/publishers/fast_publisher_cpp/README.md +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/scripts/integration/publishers/fast_py/README.md +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/scripts/integration/publishers/opensplice_publisher/README.md +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/scripts/integration/publishers/rti_c/README.md +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/scripts/integration/publishers/rti_cpp/README.md +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/__main__.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/adapters/__init__.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/adapters/base.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/adapters/common/__init__.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/adapters/common/cdr_decoder.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/adapters/common/dds_helpers.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/adapters/common/dds_introspection.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/adapters/common/discovery_tracker.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/adapters/common/endpoints.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/adapters/common/lifecycle.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/adapters/common/metrics_buffer.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/adapters/common/qos_analyzer.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/adapters/common/qos_endpoints.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/adapters/common/qos_normalize.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/adapters/common/qos_scan.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/adapters/common/topic_filter.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/adapters/common/xtypes.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/adapters/composite.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/adapters/dds_cyclone/__init__.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/adapters/dds_cyclone/adapter.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/adapters/dds_dust/__init__.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/adapters/dds_dust/adapter.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/adapters/dds_fast/__init__.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/adapters/dds_fast/adapter.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/adapters/dds_opendds/__init__.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/adapters/dds_opendds/adapter.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/adapters/ros2_live/__init__.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/adapters/ros2_live/adapter.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/adapters/ros2_live/echo_stream.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/adapters/ros2_live/parsers.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/adapters/ros2_live/process_tree.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/adapters/ros2_mock/__init__.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/adapters/ros2_mock/adapter.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/config/__init__.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/config/settings.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/constants.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/models/__init__.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/server/__init__.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/services/__init__.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/services/bag_stats.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/services/factory.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/services/health.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/services/inspector.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/services/sample_budget.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/telemetry/__init__.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/telemetry/client.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/tools/__init__.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/src/topicforge/tools/annotations.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/__init__.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/conftest.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/fixtures/ros2_echo/image_noarr.yaml +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/fixtures/ros2_echo/image_trunc4.yaml +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/fixtures/ros2_echo/scan_edge.yaml +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/fixtures/ros2_echo/scan_noarr.yaml +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/fixtures/ros2_echo/scan_trunc128.yaml +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/fixtures/ros2_echo/scan_trunc3.yaml +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/fixtures/ros2_echo/string_latched.yaml +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/fixtures/ros2_echo/twist.yaml +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/fixtures/ros2_topic_info_verbose_cmd_vel.txt +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/fixtures/ros2_topic_info_verbose_parameter_events.txt +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/fixtures/ros2_topic_info_verbose_scan.txt +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/fixtures/ros2_topic_info_verbose_tf.txt +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/fixtures/ros2_topic_info_verbose_tf_static.txt +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/integration/__init__.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/integration/ros2/Dockerfile +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/integration/ros2/__init__.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/integration/ros2/entrypoint.sh +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/integration/ros2/publisher.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/integration/ros2/run_bench.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/integration/ros2/test_live_adapter.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/integration/test_real_bus.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_analyze_bag_multi_format.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_bag_service.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_bag_stats.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_cdr_decoder.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_composite_adapter.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_config.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_cyclone_adapter.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_dds_cross_vendor.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_dds_helpers.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_dds_inactive_reason.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_dds_introspection.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_dds_qos_normalization.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_dds_schemas.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_discovery_tracker.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_dust_adapter.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_echo_parser.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_echo_stream.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_endpoints.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_example_node_spec.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_factory.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_fast_adapter.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_health.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_history_and_hints.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_honest_outputs.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_inspector.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_lifecycle_buffer.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_live_adapter_graph.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_live_adapter_parse.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_live_adapter_subprocess.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_live_sample_notes.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_metrics_buffer.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_opendds_adapter.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_peek_bag_samples.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_process_tree.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_qos_analyzer.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_qos_endpoints.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_qos_scan.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_sample_options.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_telemetry.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_tool_annotations.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_tools_integration.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_topic_metrics.py +0 -0
- {topicforge-0.6.3 → topicforge-0.6.4}/tests/test_xtypes.py +0 -0
|
@@ -7,6 +7,40 @@ and this project adheres to [Semantic Versioning 2.0.0](https://semver.org/spec/
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.6.4] - 2026-10-08
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `plugin/` is now also an Agent Plugins 1.0.0 package (open standard used by Cursor and
|
|
15
|
+
other clients): `plugin/plugin.json` and `plugin/mcp.json` next to the Claude files,
|
|
16
|
+
with the same pinned `uvx` launch and fixed env defaults (no per-user configuration in
|
|
17
|
+
that format). A repo-root `.cursor-plugin/marketplace.json` points Cursor at `plugin/`.
|
|
18
|
+
The version-consistency test covers the new files.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- `peek_bag_samples` no longer fails with "requires the `rosbags` library" for a plain
|
|
23
|
+
`pip install topicforge`, `uvx topicforge` or the Claude / Agent plugin: `rosbags` was
|
|
24
|
+
only in the `[bags]` extra, which none of those select.
|
|
25
|
+
- `sample_messages` on `/clock` (`rosgraph_msgs/Clock`), `/tf` and `/tf_static`
|
|
26
|
+
(`tf2_msgs/TFMessage`) and `/rosout` (`rcl_interfaces/Log`) returned `timestamp_ns` 0 and
|
|
27
|
+
`stamp_source` `none` although the time is in the body. It now reads `clock`,
|
|
28
|
+
`transforms[0].header.stamp` (only when the list is not empty) and `stamp`. Messages
|
|
29
|
+
with a top-level `header.stamp` are unchanged. `peek_bag_samples` applies the same rule,
|
|
30
|
+
so these topics no longer fall back to the bag record time. The mock `/tf` samples follow it.
|
|
31
|
+
|
|
32
|
+
- The MCP `initialize` handshake reported the `mcp` library version (for example 1.30.0) as
|
|
33
|
+
`serverInfo.version`; it now reports TopicForge's own version.
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- `rosbags` (pure Python, wheels for linux-aarch64 and CPython 3.14 for all of its
|
|
38
|
+
dependencies) is now a core dependency, `>=0.11.3` as before. `topicforge[bags]` stays
|
|
39
|
+
as an empty alias so existing install commands keep working.
|
|
40
|
+
- `stamp_source` accepts one additive value, `payload`: the time comes from the message
|
|
41
|
+
body (`Clock`, `TFMessage`, `Log`) rather than a `header`. Existing values are unchanged.
|
|
42
|
+
- The error raised when `rosbags` cannot be imported now gives the reinstall command.
|
|
43
|
+
|
|
10
44
|
## [0.6.3] - 2026-10-07
|
|
11
45
|
|
|
12
46
|
### Added
|
|
@@ -1354,7 +1388,8 @@ Initial MVP release of TopicForge: ROS Topic Inspector & Bag Analyzer MCP server
|
|
|
1354
1388
|
- The write path (publishing, commanding robots) is intentionally out of scope for the MVP.
|
|
1355
1389
|
- `analyze_bag` in live mode parses `ros2 bag info` text output; deeper anomaly detection remains mock-only for now.
|
|
1356
1390
|
|
|
1357
|
-
[Unreleased]: https://github.com/yaniswav/TopicForge/compare/v0.6.
|
|
1391
|
+
[Unreleased]: https://github.com/yaniswav/TopicForge/compare/v0.6.4...HEAD
|
|
1392
|
+
[0.6.4]: https://github.com/yaniswav/TopicForge/compare/v0.6.3...v0.6.4
|
|
1358
1393
|
[0.6.3]: https://github.com/yaniswav/TopicForge/compare/v0.6.2...v0.6.3
|
|
1359
1394
|
[0.6.2]: https://github.com/yaniswav/TopicForge/compare/v0.6.1...v0.6.2
|
|
1360
1395
|
[0.6.1]: https://github.com/yaniswav/TopicForge/compare/v0.6.0...v0.6.1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: topicforge
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.4
|
|
4
4
|
Summary: ROS Topic Inspector & Bag Analyzer MCP server for AI agents
|
|
5
5
|
Project-URL: Homepage, https://github.com/yaniswav/TopicForge
|
|
6
6
|
Project-URL: Repository, https://github.com/yaniswav/TopicForge
|
|
@@ -44,10 +44,10 @@ Requires-Dist: cyclonedds>=0.10; python_version < '3.14' and ((platform_system =
|
|
|
44
44
|
Requires-Dist: mcp<2,>=1.0.0
|
|
45
45
|
Requires-Dist: pydantic>=2.6
|
|
46
46
|
Requires-Dist: pyyaml>=6
|
|
47
|
+
Requires-Dist: rosbags>=0.11.3
|
|
47
48
|
Provides-Extra: all
|
|
48
49
|
Requires-Dist: cyclonedds>=0.10; extra == 'all'
|
|
49
50
|
Provides-Extra: bags
|
|
50
|
-
Requires-Dist: rosbags>=0.11.3; extra == 'bags'
|
|
51
51
|
Provides-Extra: dds
|
|
52
52
|
Requires-Dist: cyclonedds>=0.10; extra == 'dds'
|
|
53
53
|
Provides-Extra: dds-cyclone
|
|
@@ -55,7 +55,6 @@ Requires-Dist: cyclonedds>=0.10; extra == 'dds-cyclone'
|
|
|
55
55
|
Provides-Extra: dev
|
|
56
56
|
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
57
57
|
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
58
|
-
Requires-Dist: rosbags>=0.11.3; extra == 'dev'
|
|
59
58
|
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
60
59
|
Description-Content-Type: text/markdown
|
|
61
60
|
|
|
@@ -103,7 +102,7 @@ The server speaks MCP over stdio and waits for a client, so wire it into one. Fo
|
|
|
103
102
|
|
|
104
103
|
Then ask it to list the topics or to analyze `/tmp/demo.mcap`. For Claude Code: `claude mcp add topicforge -- topicforge`.
|
|
105
104
|
|
|
106
|
-
**Other MCP clients.** Ready-to-paste configs for Claude Desktop, Cursor, VS Code, Windsurf, Cline, Roo Code, Continue, Zed, JetBrains, Codex, Gemini CLI, Goose and more are in [`docs/CLIENTS.md`](docs/CLIENTS.md). One-click: [Add to Cursor](https://cursor.com/install-mcp?name=topicforge&config=
|
|
105
|
+
**Other MCP clients.** Ready-to-paste configs for Claude Desktop, Cursor, VS Code, Windsurf, Cline, Roo Code, Continue, Zed, JetBrains, Codex, Gemini CLI, Goose and more are in [`docs/CLIENTS.md`](docs/CLIENTS.md). One-click: [Add to Cursor](https://cursor.com/install-mcp?name=topicforge&config=eyJjb21tYW5kIjoidXZ4IiwiYXJncyI6WyItLWZyb20iLCJ0b3BpY2ZvcmdlPT0wLjYuNCIsInRvcGljZm9yZ2UiXSwiZW52Ijp7IlRPUElDRk9SR0VfTU9ERSI6ImF1dG8iLCJUT1BJQ0ZPUkdFX0REU19CQUNLRU5EIjoiY3ljbG9uZSIsIlRPUElDRk9SR0VfRERTX0RPTUFJTl9JRCI6IjAifX0%3D) | [Install in VS Code](https://vscode.dev/redirect/mcp/install?name=topicforge&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22--from%22%2C%22topicforge%3D%3D0.6.4%22%2C%22topicforge%22%5D%2C%22env%22%3A%7B%22TOPICFORGE_MODE%22%3A%22auto%22%2C%22TOPICFORGE_DDS_BACKEND%22%3A%22cyclone%22%2C%22TOPICFORGE_DDS_DOMAIN_ID%22%3A%220%22%7D%7D).
|
|
107
106
|
|
|
108
107
|
Setup for a real ROS2 environment (WSL2, Linux, Docker, native Windows) is in [`docs/TESTING.md`](docs/TESTING.md); recurring monitoring prompts and the privacy contract are in [`docs/TUTORIEL.md`](docs/TUTORIEL.md).
|
|
109
108
|
|
|
@@ -123,7 +122,7 @@ Every response except `health_check` carries `mode_effective` (`"live"` or `"moc
|
|
|
123
122
|
| `peek_dds_samples` | Raw DDS samples; structured on the three builtin discovery topics, presence-only on user topics |
|
|
124
123
|
| `participant_events` | Timeline of participant `discovered` / `lost` events |
|
|
125
124
|
| `topic_metrics` | Frequency, sequence-gap and latency schema; data only for builtin discovery topics |
|
|
126
|
-
| `peek_bag_samples` | Decoded samples from a recorded bag, including ROS 1 `.bag` (
|
|
125
|
+
| `peek_bag_samples` | Decoded samples from a recorded bag, including ROS 1 `.bag` (`rosbags` ships with the package) |
|
|
127
126
|
| `list_endpoints` | DDS writers and readers with structured QoS, per-topic roll-up that flags orphans (writer with no reader, reader with no writer) |
|
|
128
127
|
|
|
129
128
|
Walkthroughs against the mock, each with the exact tool calls and payloads, are in [`examples/`](examples/README.md). To run the DDS tools against a real bus with several programs and vendors, see [`examples/dds/README.md`](examples/dds/README.md) (`python examples/dds/run_all.py`).
|
|
@@ -175,8 +174,8 @@ External validation against a simulated robot's ground truth: [docs/VALIDATION.m
|
|
|
175
174
|
- Single domain: the server observes the domain it joined at startup; changing it needs a restart.
|
|
176
175
|
- DDS Security is not handled. A participant without credentials sees an empty secure bus. `detect_qos_mismatches` checks Partition, type name, Reliability, Durability, Deadline, Liveliness, LatencyBudget, Ownership (kind), DestinationOrder and DataRepresentation (History as a risk); Presentation, XTypes assignability and runtime behavior are not checked, and the result lists them in `policies_unchecked`. It returns a `MismatchScan` envelope: read `reports` for the mismatches.
|
|
177
176
|
- Fast DDS serves no `list_endpoints`.
|
|
178
|
-
- `sample_messages` (live) streams `ros2 topic echo` until `count` messages arrive or `timeout_s` (1..45, default 10, for the whole call) runs out, and returns what arrived with a `note` (`N of M messages within T s`, saying whether a publisher exists). QoS is matched to the publishers, so latched topics work. The payload has nested named fields (`payload.header.stamp.sec`); `timestamp_ns` is `header.stamp` (the publisher's clock, sim time on a simulation) or 0 for headerless types, with `stamp_source` and `received_ns` (wall clock when the CLI printed it). Arrays are cut at 128 elements by default; `max_array_length` (1..65536, or null for no cut) and `arrays_summary_only` change that, and a cut is listed under `_truncated_fields`. `nan` and `inf` come back as strings. `count` above 50 is capped with a note.
|
|
179
|
-
- `analyze_bag` (live) parses `ros2 bag info` text for the totals and counts; anomaly detection is mock-only. Per-topic times, rates (`(n - 1) / span`) and `latched` are added when the bag can be read locally (`.db3` with the standard library, `.mcap` with `rosbags`), else rates fall back to count / bag duration (`frequency_basis`). `peek_bag_samples` reads the file itself, through `rosbags`, returns the first `count` messages in recording order (not the last), and is served only by the ROS2 CLI adapter or the mock; its `timestamp_ns` is the message's `header.stamp` (`stamp_source` `header`) or, without
|
|
177
|
+
- `sample_messages` (live) streams `ros2 topic echo` until `count` messages arrive or `timeout_s` (1..45, default 10, for the whole call) runs out, and returns what arrived with a `note` (`N of M messages within T s`, saying whether a publisher exists). QoS is matched to the publishers, so latched topics work. The payload has nested named fields (`payload.header.stamp.sec`); `timestamp_ns` is `header.stamp` (the publisher's clock, sim time on a simulation), the time in the body of `/clock`, `/tf` and `/rosout` messages (`stamp_source` `payload`), or 0 for other headerless types, with `stamp_source` and `received_ns` (wall clock when the CLI printed it). Arrays are cut at 128 elements by default; `max_array_length` (1..65536, or null for no cut) and `arrays_summary_only` change that, and a cut is listed under `_truncated_fields`. `nan` and `inf` come back as strings. `count` above 50 is capped with a note.
|
|
178
|
+
- `analyze_bag` (live) parses `ros2 bag info` text for the totals and counts; anomaly detection is mock-only. Per-topic times, rates (`(n - 1) / span`) and `latched` are added when the bag can be read locally (`.db3` with the standard library, `.mcap` with `rosbags`), else rates fall back to count / bag duration (`frequency_basis`). `peek_bag_samples` reads the file itself, through `rosbags`, returns the first `count` messages in recording order (not the last), and is served only by the ROS2 CLI adapter or the mock; its `timestamp_ns` is the message's `header.stamp` (`stamp_source` `header`), the body time of `Clock`, `TFMessage` and `Log` messages (`payload`) or, without either, the bag record time (`stamp_source` `recorded`), and `recorded_ns` is always the bag record time; bags that embed no message definitions (Humble `.db3`) are decoded with the Humble definitions, or the distro the bag records, and `note` says so. Without `ros2`, bag tools return fixtures: check `health_check` for `mode: "mock"` before trusting bag output. `health_check` also reports `sim_clock_published` (live only): true when `/clock` has a publisher, a hint that header stamps may be simulation time.
|
|
180
179
|
- Synchronous handlers: the tools run on the MCP event loop; on Windows a hung `ros2` launcher can block the server.
|
|
181
180
|
- No streaming or push subscriptions: tools are strictly request/response.
|
|
182
181
|
|
|
@@ -199,7 +198,7 @@ When on, each tool call emits one event with exactly six fields:
|
|
|
199
198
|
| `tool_name` | `"list_topics"` | One of the twelve tools, never argument values |
|
|
200
199
|
| `latency_ms` | `12.34` | Handler wall-clock duration, 2 decimals |
|
|
201
200
|
| `mode` | `"mock"` | Mode of the adapter actually serving: `mock` or `live` |
|
|
202
|
-
| `version` | `"0.6.
|
|
201
|
+
| `version` | `"0.6.4"` | TopicForge server version |
|
|
203
202
|
| `session_id` | `"a1b2c3..."` | Random UUID per process, never persisted |
|
|
204
203
|
| `success` | `true` | Whether the handler returned or raised |
|
|
205
204
|
|
|
@@ -42,7 +42,7 @@ The server speaks MCP over stdio and waits for a client, so wire it into one. Fo
|
|
|
42
42
|
|
|
43
43
|
Then ask it to list the topics or to analyze `/tmp/demo.mcap`. For Claude Code: `claude mcp add topicforge -- topicforge`.
|
|
44
44
|
|
|
45
|
-
**Other MCP clients.** Ready-to-paste configs for Claude Desktop, Cursor, VS Code, Windsurf, Cline, Roo Code, Continue, Zed, JetBrains, Codex, Gemini CLI, Goose and more are in [`docs/CLIENTS.md`](docs/CLIENTS.md). One-click: [Add to Cursor](https://cursor.com/install-mcp?name=topicforge&config=
|
|
45
|
+
**Other MCP clients.** Ready-to-paste configs for Claude Desktop, Cursor, VS Code, Windsurf, Cline, Roo Code, Continue, Zed, JetBrains, Codex, Gemini CLI, Goose and more are in [`docs/CLIENTS.md`](docs/CLIENTS.md). One-click: [Add to Cursor](https://cursor.com/install-mcp?name=topicforge&config=eyJjb21tYW5kIjoidXZ4IiwiYXJncyI6WyItLWZyb20iLCJ0b3BpY2ZvcmdlPT0wLjYuNCIsInRvcGljZm9yZ2UiXSwiZW52Ijp7IlRPUElDRk9SR0VfTU9ERSI6ImF1dG8iLCJUT1BJQ0ZPUkdFX0REU19CQUNLRU5EIjoiY3ljbG9uZSIsIlRPUElDRk9SR0VfRERTX0RPTUFJTl9JRCI6IjAifX0%3D) | [Install in VS Code](https://vscode.dev/redirect/mcp/install?name=topicforge&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22--from%22%2C%22topicforge%3D%3D0.6.4%22%2C%22topicforge%22%5D%2C%22env%22%3A%7B%22TOPICFORGE_MODE%22%3A%22auto%22%2C%22TOPICFORGE_DDS_BACKEND%22%3A%22cyclone%22%2C%22TOPICFORGE_DDS_DOMAIN_ID%22%3A%220%22%7D%7D).
|
|
46
46
|
|
|
47
47
|
Setup for a real ROS2 environment (WSL2, Linux, Docker, native Windows) is in [`docs/TESTING.md`](docs/TESTING.md); recurring monitoring prompts and the privacy contract are in [`docs/TUTORIEL.md`](docs/TUTORIEL.md).
|
|
48
48
|
|
|
@@ -62,7 +62,7 @@ Every response except `health_check` carries `mode_effective` (`"live"` or `"moc
|
|
|
62
62
|
| `peek_dds_samples` | Raw DDS samples; structured on the three builtin discovery topics, presence-only on user topics |
|
|
63
63
|
| `participant_events` | Timeline of participant `discovered` / `lost` events |
|
|
64
64
|
| `topic_metrics` | Frequency, sequence-gap and latency schema; data only for builtin discovery topics |
|
|
65
|
-
| `peek_bag_samples` | Decoded samples from a recorded bag, including ROS 1 `.bag` (
|
|
65
|
+
| `peek_bag_samples` | Decoded samples from a recorded bag, including ROS 1 `.bag` (`rosbags` ships with the package) |
|
|
66
66
|
| `list_endpoints` | DDS writers and readers with structured QoS, per-topic roll-up that flags orphans (writer with no reader, reader with no writer) |
|
|
67
67
|
|
|
68
68
|
Walkthroughs against the mock, each with the exact tool calls and payloads, are in [`examples/`](examples/README.md). To run the DDS tools against a real bus with several programs and vendors, see [`examples/dds/README.md`](examples/dds/README.md) (`python examples/dds/run_all.py`).
|
|
@@ -114,8 +114,8 @@ External validation against a simulated robot's ground truth: [docs/VALIDATION.m
|
|
|
114
114
|
- Single domain: the server observes the domain it joined at startup; changing it needs a restart.
|
|
115
115
|
- DDS Security is not handled. A participant without credentials sees an empty secure bus. `detect_qos_mismatches` checks Partition, type name, Reliability, Durability, Deadline, Liveliness, LatencyBudget, Ownership (kind), DestinationOrder and DataRepresentation (History as a risk); Presentation, XTypes assignability and runtime behavior are not checked, and the result lists them in `policies_unchecked`. It returns a `MismatchScan` envelope: read `reports` for the mismatches.
|
|
116
116
|
- Fast DDS serves no `list_endpoints`.
|
|
117
|
-
- `sample_messages` (live) streams `ros2 topic echo` until `count` messages arrive or `timeout_s` (1..45, default 10, for the whole call) runs out, and returns what arrived with a `note` (`N of M messages within T s`, saying whether a publisher exists). QoS is matched to the publishers, so latched topics work. The payload has nested named fields (`payload.header.stamp.sec`); `timestamp_ns` is `header.stamp` (the publisher's clock, sim time on a simulation) or 0 for headerless types, with `stamp_source` and `received_ns` (wall clock when the CLI printed it). Arrays are cut at 128 elements by default; `max_array_length` (1..65536, or null for no cut) and `arrays_summary_only` change that, and a cut is listed under `_truncated_fields`. `nan` and `inf` come back as strings. `count` above 50 is capped with a note.
|
|
118
|
-
- `analyze_bag` (live) parses `ros2 bag info` text for the totals and counts; anomaly detection is mock-only. Per-topic times, rates (`(n - 1) / span`) and `latched` are added when the bag can be read locally (`.db3` with the standard library, `.mcap` with `rosbags`), else rates fall back to count / bag duration (`frequency_basis`). `peek_bag_samples` reads the file itself, through `rosbags`, returns the first `count` messages in recording order (not the last), and is served only by the ROS2 CLI adapter or the mock; its `timestamp_ns` is the message's `header.stamp` (`stamp_source` `header`) or, without
|
|
117
|
+
- `sample_messages` (live) streams `ros2 topic echo` until `count` messages arrive or `timeout_s` (1..45, default 10, for the whole call) runs out, and returns what arrived with a `note` (`N of M messages within T s`, saying whether a publisher exists). QoS is matched to the publishers, so latched topics work. The payload has nested named fields (`payload.header.stamp.sec`); `timestamp_ns` is `header.stamp` (the publisher's clock, sim time on a simulation), the time in the body of `/clock`, `/tf` and `/rosout` messages (`stamp_source` `payload`), or 0 for other headerless types, with `stamp_source` and `received_ns` (wall clock when the CLI printed it). Arrays are cut at 128 elements by default; `max_array_length` (1..65536, or null for no cut) and `arrays_summary_only` change that, and a cut is listed under `_truncated_fields`. `nan` and `inf` come back as strings. `count` above 50 is capped with a note.
|
|
118
|
+
- `analyze_bag` (live) parses `ros2 bag info` text for the totals and counts; anomaly detection is mock-only. Per-topic times, rates (`(n - 1) / span`) and `latched` are added when the bag can be read locally (`.db3` with the standard library, `.mcap` with `rosbags`), else rates fall back to count / bag duration (`frequency_basis`). `peek_bag_samples` reads the file itself, through `rosbags`, returns the first `count` messages in recording order (not the last), and is served only by the ROS2 CLI adapter or the mock; its `timestamp_ns` is the message's `header.stamp` (`stamp_source` `header`), the body time of `Clock`, `TFMessage` and `Log` messages (`payload`) or, without either, the bag record time (`stamp_source` `recorded`), and `recorded_ns` is always the bag record time; bags that embed no message definitions (Humble `.db3`) are decoded with the Humble definitions, or the distro the bag records, and `note` says so. Without `ros2`, bag tools return fixtures: check `health_check` for `mode: "mock"` before trusting bag output. `health_check` also reports `sim_clock_published` (live only): true when `/clock` has a publisher, a hint that header stamps may be simulation time.
|
|
119
119
|
- Synchronous handlers: the tools run on the MCP event loop; on Windows a hung `ros2` launcher can block the server.
|
|
120
120
|
- No streaming or push subscriptions: tools are strictly request/response.
|
|
121
121
|
|
|
@@ -138,7 +138,7 @@ When on, each tool call emits one event with exactly six fields:
|
|
|
138
138
|
| `tool_name` | `"list_topics"` | One of the twelve tools, never argument values |
|
|
139
139
|
| `latency_ms` | `12.34` | Handler wall-clock duration, 2 decimals |
|
|
140
140
|
| `mode` | `"mock"` | Mode of the adapter actually serving: `mock` or `live` |
|
|
141
|
-
| `version` | `"0.6.
|
|
141
|
+
| `version` | `"0.6.4"` | TopicForge server version |
|
|
142
142
|
| `session_id` | `"a1b2c3..."` | Random UUID per process, never persisted |
|
|
143
143
|
| `success` | `true` | Whether the handler returned or raised |
|
|
144
144
|
|
|
@@ -6,7 +6,7 @@ TopicForge (ROS 2 / DDS) is a local, read-only MCP server that speaks stdio.
|
|
|
6
6
|
Every client below launches it as a child process with the same command:
|
|
7
7
|
|
|
8
8
|
```
|
|
9
|
-
uvx --from "topicforge==0.6.
|
|
9
|
+
uvx --from "topicforge==0.6.4" topicforge
|
|
10
10
|
```
|
|
11
11
|
|
|
12
12
|
and the same three environment variables:
|
|
@@ -21,7 +21,7 @@ The registry name is `io.github.yaniswav/topicforge`. Where a client asks for a
|
|
|
21
21
|
display name, use **TopicForge (ROS 2 / DDS)**: an unrelated SEO product is also
|
|
22
22
|
called TopicForge in the MCP registries.
|
|
23
23
|
|
|
24
|
-
The version pin (`==0.6.
|
|
24
|
+
The version pin (`==0.6.4`) is moved on every release together with the other
|
|
25
25
|
version strings; a test keeps them in sync.
|
|
26
26
|
|
|
27
27
|
## Before you start
|
|
@@ -44,7 +44,7 @@ version strings; a test keeps them in sync.
|
|
|
44
44
|
`"--from"` in the args. To build the binding yourself, install the Cyclone C library and
|
|
45
45
|
run `pip install "topicforge[dds]"`. The `[dds]` and `[dds-cyclone]` extras still work
|
|
46
46
|
as aliases, so older commands keep installing.
|
|
47
|
-
- **pip alternative.** `pip install "topicforge==0.6.
|
|
47
|
+
- **pip alternative.** `pip install "topicforge==0.6.4"`, then use
|
|
48
48
|
`"command": "topicforge"` (or `"command": "python", "args": ["-m", "topicforge"]`)
|
|
49
49
|
with no `args` for uvx. Use the absolute path of the binary if the client does
|
|
50
50
|
not see your venv.
|
|
@@ -72,7 +72,7 @@ say where the file lives and what differs.
|
|
|
72
72
|
"mcpServers": {
|
|
73
73
|
"topicforge": {
|
|
74
74
|
"command": "uvx",
|
|
75
|
-
"args": ["--from", "topicforge==0.6.
|
|
75
|
+
"args": ["--from", "topicforge==0.6.4", "topicforge"],
|
|
76
76
|
"env": {
|
|
77
77
|
"TOPICFORGE_MODE": "auto",
|
|
78
78
|
"TOPICFORGE_DDS_BACKEND": "cyclone",
|
|
@@ -95,7 +95,7 @@ claude plugin install topicforge@topicforge
|
|
|
95
95
|
Or just the server:
|
|
96
96
|
|
|
97
97
|
```
|
|
98
|
-
claude mcp add topicforge --env TOPICFORGE_MODE=auto --env TOPICFORGE_DDS_BACKEND=cyclone --env TOPICFORGE_DDS_DOMAIN_ID=0 -- uvx --from "topicforge==0.6.
|
|
98
|
+
claude mcp add topicforge --env TOPICFORGE_MODE=auto --env TOPICFORGE_DDS_BACKEND=cyclone --env TOPICFORGE_DDS_DOMAIN_ID=0 -- uvx --from "topicforge==0.6.4" topicforge
|
|
99
99
|
```
|
|
100
100
|
|
|
101
101
|
## Claude Desktop
|
|
@@ -112,18 +112,22 @@ the common JSON shape above and restart the app completely. On Windows, if
|
|
|
112
112
|
|
|
113
113
|
The one-click links in this page and in the README are https redirects, because GitHub and PyPI strip `cursor://` and `vscode:` links. Only the raw schemes are documented by Cursor and VS Code; the https forms (`cursor.com/install-mcp`, `vscode.dev/redirect/mcp/install`) were observed to redirect correctly on 2026-10-06 but are not in the vendors' docs (unverified, last checked 2026-10-06). The raw links are given as a fallback.
|
|
114
114
|
|
|
115
|
-
One click: [Add TopicForge to Cursor](https://cursor.com/install-mcp?name=topicforge&config=
|
|
115
|
+
One click: [Add TopicForge to Cursor](https://cursor.com/install-mcp?name=topicforge&config=eyJjb21tYW5kIjoidXZ4IiwiYXJncyI6WyItLWZyb20iLCJ0b3BpY2ZvcmdlPT0wLjYuNCIsInRvcGljZm9yZ2UiXSwiZW52Ijp7IlRPUElDRk9SR0VfTU9ERSI6ImF1dG8iLCJUT1BJQ0ZPUkdFX0REU19CQUNLRU5EIjoiY3ljbG9uZSIsIlRPUElDRk9SR0VfRERTX0RPTUFJTl9JRCI6IjAifX0%3D)
|
|
116
116
|
|
|
117
|
-
If that page does not open Cursor, use the raw deeplink: `cursor://anysphere.cursor-deeplink/mcp/install?name=topicforge&config=
|
|
117
|
+
If that page does not open Cursor, use the raw deeplink: `cursor://anysphere.cursor-deeplink/mcp/install?name=topicforge&config=eyJjb21tYW5kIjoidXZ4IiwiYXJncyI6WyItLWZyb20iLCJ0b3BpY2ZvcmdlPT0wLjYuNCIsInRvcGljZm9yZ2UiXSwiZW52Ijp7IlRPUElDRk9SR0VfTU9ERSI6ImF1dG8iLCJUT1BJQ0ZPUkdFX0REU19CQUNLRU5EIjoiY3ljbG9uZSIsIlRPUElDRk9SR0VfRERTX0RPTUFJTl9JRCI6IjAifX0=`
|
|
118
118
|
|
|
119
119
|
Or paste the common JSON shape into `~/.cursor/mcp.json` (all projects) or
|
|
120
120
|
`.cursor/mcp.json` (one project).
|
|
121
121
|
|
|
122
|
+
## Agent Plugins (Cursor and other compatible clients)
|
|
123
|
+
|
|
124
|
+
The `plugin/` folder of this repository is also an [Agent Plugins](https://agent-plugins.org) 1.0.0 package: `plugin.json` (manifest), `mcp.json` (the same pinned `uvx` launch) and `skills/` (two skills). Clients that load that format, such as Cursor, can install it as a plugin and get the server and the skills in one step; the plain JSON config above remains the simplest route for everything else. The format has no user-configuration prompt, so the backend is fixed to `cyclone` and the domain to `0`; edit the `env` block of `mcp.json` to change them (see `plugin/README.md`). Cursor Directory (cursor.directory/plugins/new) and the Cursor marketplace take a repository URL; a repo-root `.cursor-plugin/marketplace.json` points Cursor at the `plugin/` subfolder. Whether each directory scanner accepts a subfolder is unverified.
|
|
125
|
+
|
|
122
126
|
## VS Code / GitHub Copilot
|
|
123
127
|
|
|
124
|
-
One click: [Install in VS Code](https://vscode.dev/redirect/mcp/install?name=topicforge&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22--from%22%2C%22topicforge%3D%3D0.6.
|
|
128
|
+
One click: [Install in VS Code](https://vscode.dev/redirect/mcp/install?name=topicforge&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22--from%22%2C%22topicforge%3D%3D0.6.4%22%2C%22topicforge%22%5D%2C%22env%22%3A%7B%22TOPICFORGE_MODE%22%3A%22auto%22%2C%22TOPICFORGE_DDS_BACKEND%22%3A%22cyclone%22%2C%22TOPICFORGE_DDS_DOMAIN_ID%22%3A%220%22%7D%7D)
|
|
125
129
|
|
|
126
|
-
Fallback, the raw URL handler: `vscode:mcp/install?%7B%22name%22%3A%22topicforge%22%2C%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22--from%22%2C%22topicforge%3D%3D0.6.
|
|
130
|
+
Fallback, the raw URL handler: `vscode:mcp/install?%7B%22name%22%3A%22topicforge%22%2C%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22--from%22%2C%22topicforge%3D%3D0.6.4%22%2C%22topicforge%22%5D%2C%22env%22%3A%7B%22TOPICFORGE_MODE%22%3A%22auto%22%2C%22TOPICFORGE_DDS_BACKEND%22%3A%22cyclone%22%2C%22TOPICFORGE_DDS_DOMAIN_ID%22%3A%220%22%7D%7D`
|
|
127
131
|
|
|
128
132
|
Or put this in `.vscode/mcp.json` (the key is `servers`, not `mcpServers`), or run
|
|
129
133
|
"MCP: Add Server" from the Command Palette:
|
|
@@ -134,7 +138,7 @@ Or put this in `.vscode/mcp.json` (the key is `servers`, not `mcpServers`), or r
|
|
|
134
138
|
"topicforge": {
|
|
135
139
|
"type": "stdio",
|
|
136
140
|
"command": "uvx",
|
|
137
|
-
"args": ["--from", "topicforge==0.6.
|
|
141
|
+
"args": ["--from", "topicforge==0.6.4", "topicforge"],
|
|
138
142
|
"env": {
|
|
139
143
|
"TOPICFORGE_MODE": "auto",
|
|
140
144
|
"TOPICFORGE_DDS_BACKEND": "cyclone",
|
|
@@ -163,7 +167,7 @@ Servers", and paste the common JSON shape (the file is
|
|
|
163
167
|
Project file `.roo/mcp.json` (takes precedence) or the global `mcp_settings.json`
|
|
164
168
|
opened from the MCP view. Paste the common JSON shape. Roo's docs say Windows
|
|
165
169
|
needs a `cmd /c` wrapper for `npx`; for `uvx`, try it directly first and fall back
|
|
166
|
-
to `"command": "cmd", "args": ["/c", "uvx", "--from", "topicforge==0.6.
|
|
170
|
+
to `"command": "cmd", "args": ["/c", "uvx", "--from", "topicforge==0.6.4",
|
|
167
171
|
"topicforge"]` if it fails to start.
|
|
168
172
|
|
|
169
173
|
## Continue
|
|
@@ -180,7 +184,7 @@ mcpServers:
|
|
|
180
184
|
command: uvx
|
|
181
185
|
args:
|
|
182
186
|
- "--from"
|
|
183
|
-
- "topicforge==0.6.
|
|
187
|
+
- "topicforge==0.6.4"
|
|
184
188
|
- "topicforge"
|
|
185
189
|
env:
|
|
186
190
|
TOPICFORGE_MODE: auto
|
|
@@ -197,7 +201,7 @@ In `settings.json`, under `context_servers`:
|
|
|
197
201
|
"context_servers": {
|
|
198
202
|
"topicforge": {
|
|
199
203
|
"command": "uvx",
|
|
200
|
-
"args": ["--from", "topicforge==0.6.
|
|
204
|
+
"args": ["--from", "topicforge==0.6.4", "topicforge"],
|
|
201
205
|
"env": {
|
|
202
206
|
"TOPICFORGE_MODE": "auto",
|
|
203
207
|
"TOPICFORGE_DDS_BACKEND": "cyclone",
|
|
@@ -223,7 +227,7 @@ run with the dds extra can exceed, so set it to 60 s:
|
|
|
223
227
|
```toml
|
|
224
228
|
[mcp_servers.topicforge]
|
|
225
229
|
command = "uvx"
|
|
226
|
-
args = ["--from", "topicforge==0.6.
|
|
230
|
+
args = ["--from", "topicforge==0.6.4", "topicforge"]
|
|
227
231
|
startup_timeout_sec = 60
|
|
228
232
|
|
|
229
233
|
[mcp_servers.topicforge.env]
|
|
@@ -254,7 +258,7 @@ extensions:
|
|
|
254
258
|
name: TopicForge (ROS 2 / DDS)
|
|
255
259
|
type: stdio
|
|
256
260
|
cmd: uvx
|
|
257
|
-
args: [--from, "topicforge==0.6.
|
|
261
|
+
args: [--from, "topicforge==0.6.4", topicforge]
|
|
258
262
|
enabled: true
|
|
259
263
|
envs: { "TOPICFORGE_MODE": "auto", "TOPICFORGE_DDS_BACKEND": "cyclone", "TOPICFORGE_DDS_DOMAIN_ID": "0" }
|
|
260
264
|
timeout: 300
|
|
@@ -279,7 +283,7 @@ Settings > MCP > "+ Add", then paste the common JSON shape (unverified, last che
|
|
|
279
283
|
|
|
280
284
|
## Troubleshooting
|
|
281
285
|
|
|
282
|
-
- Run the command by hand first: `uvx --from "topicforge==0.6.
|
|
286
|
+
- Run the command by hand first: `uvx --from "topicforge==0.6.4" topicforge --version`.
|
|
283
287
|
- Server starts but sees no DDS participants: check the domain id, and that the
|
|
284
288
|
client machine is on the same network as the robot (DDS discovery uses
|
|
285
289
|
multicast UDP). Ask the assistant to call `health_check`.
|
|
@@ -15,7 +15,7 @@ How to get a working ROS2 environment to point TopicForge at, and how to wire it
|
|
|
15
15
|
```bash
|
|
16
16
|
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\Activate.ps1
|
|
17
17
|
pip install topicforge
|
|
18
|
-
python -m topicforge --version # -> topicforge 0.6.
|
|
18
|
+
python -m topicforge --version # -> topicforge 0.6.4
|
|
19
19
|
TOPICFORGE_MODE=mock python -m topicforge # blocks on stdio; MCP clients spawn it
|
|
20
20
|
```
|
|
21
21
|
|
|
@@ -41,7 +41,7 @@ python3 -m venv ~/topicforge-venv && source ~/topicforge-venv/bin/activate
|
|
|
41
41
|
pip install topicforge # Ubuntu 22.04 ships Python 3.10, which is enough
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
Then run live mode with three terminals. In the first, `ros2 run demo_nodes_cpp talker` publishes `/chatter` at about 1 Hz. In the second, `ros2 topic list` should show `/chatter`. In the third, source ROS2, activate the venv and run `TOPICFORGE_MODE=live python -m topicforge`. The startup line reads `topicforge 0.6.
|
|
44
|
+
Then run live mode with three terminals. In the first, `ros2 run demo_nodes_cpp talker` publishes `/chatter` at about 1 Hz. In the second, `ros2 topic list` should show `/chatter`. In the third, source ROS2, activate the venv and run `TOPICFORGE_MODE=live python -m topicforge`. The startup line reads `topicforge 0.6.4 ready (mode=live, requested_mode=live, adapter=ros2_cli, telemetry=off)`; `mode=mock` means `ros2` was not found and the server fell back to fixtures.
|
|
45
45
|
|
|
46
46
|
## Linux native
|
|
47
47
|
|
|
@@ -77,7 +77,7 @@ TopicForge resolves the `ros2` launcher with `shutil.which` (normally `ros2.exe`
|
|
|
77
77
|
|
|
78
78
|
Discover the graph. Ask "What topics are currently being published, and what message types do they carry?" TopicForge calls `list_topics`. Live mode shows `/chatter` plus `/rosout` and `/parameter_events`; mock mode shows `/cmd_vel`, `/odom`, `/scan`, `/tf` and `/camera/image_raw`.
|
|
79
79
|
|
|
80
|
-
Inspect and sample. Ask "Show me the latest message on /chatter." TopicForge calls `get_topic_info` then `sample_messages`. The payload has the message fields as nested named values. `timestamp_ns` is the `header.stamp` and `0` for headerless types such as `std_msgs/String` (`stamp_source` says which).
|
|
80
|
+
Inspect and sample. Ask "Show me the latest message on /chatter." TopicForge calls `get_topic_info` then `sample_messages`. The payload has the message fields as nested named values. `timestamp_ns` is the `header.stamp` and the body time of `/clock`, `/tf` and `/rosout`, and `0` for other headerless types such as `std_msgs/String` (`stamp_source` says which).
|
|
81
81
|
|
|
82
82
|
Record and analyze a bag.
|
|
83
83
|
|
|
@@ -85,7 +85,7 @@ Record and analyze a bag.
|
|
|
85
85
|
ros2 bag record /chatter --output ~/demo_run --max-bag-duration 30
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
-
Then ask "Analyze the bag at /home/<you>/demo_run." `analyze_bag` returns duration, message count and per-topic stats; anomalies are mock-only. To read actual messages,
|
|
88
|
+
Then ask "Analyze the bag at /home/<you>/demo_run." `analyze_bag` returns duration, message count and per-topic stats; anomalies are mock-only. To read actual messages, ask for samples, which calls `peek_bag_samples`. A fourth scenario, DDS discovery and QoS diagnosis against a real bus, is in [`examples/dds/README.md`](../examples/dds/README.md).
|
|
89
89
|
|
|
90
90
|
## Connect an MCP client
|
|
91
91
|
|
|
@@ -35,7 +35,7 @@ If you want a DDS-only deployment, the message is expected: you use the seven DD
|
|
|
35
35
|
|
|
36
36
|
## "rosbags"
|
|
37
37
|
|
|
38
|
-
`peek_bag_samples` needs the
|
|
38
|
+
`peek_bag_samples` needs the `rosbags` library, a core dependency since 0.6.4 (older versions: `pip install topicforge[bags]`, which still works as an alias); if the import fails, `pip install --force-reinstall rosbags`, then retry. `analyze_bag` does not raise this error, since in live mode it parses `ros2 bag info` and never touches `rosbags`; the error text mentions an `analyze_bag` fallback that does not exist. `peek_bag_samples` is served only by the ROS2 CLI adapter and the mock. Without `ros2` and without a DDS backend you are on the mock and get fixture samples, not the content of your file: check `health_check` first.
|
|
39
39
|
|
|
40
40
|
## "CycloneDDS participant discovery failed"
|
|
41
41
|
|
|
@@ -126,7 +126,7 @@ Regardless of the telemetry setting, TopicForge never transmits topic names, mes
|
|
|
126
126
|
|
|
127
127
|
### Bags are read locally
|
|
128
128
|
|
|
129
|
-
`analyze_bag` and `peek_bag_samples` read bag files on the filesystem where TopicForge runs. `analyze_bag` runs `ros2 bag info` on the path in live mode; `peek_bag_samples` parses the file in-process with the `rosbags` library (
|
|
129
|
+
`analyze_bag` and `peek_bag_samples` read bag files on the filesystem where TopicForge runs. `analyze_bag` runs `ros2 bag info` on the path in live mode; `peek_bag_samples` parses the file in-process with the `rosbags` library (installed with topicforge). Nothing about a bag's content or path is uploaded anywhere.
|
|
130
130
|
|
|
131
131
|
---
|
|
132
132
|
|
|
@@ -4,7 +4,7 @@ TopicForge is a read-only MCP server for ROS 2 and DDS. This plugin bundles the
|
|
|
4
4
|
|
|
5
5
|
## What is included
|
|
6
6
|
|
|
7
|
-
- MCP server `topicforge`, started with `uvx` from the `topicforge` package on PyPI (version pinned to 0.6.
|
|
7
|
+
- MCP server `topicforge`, started with `uvx` from the `topicforge` package on PyPI (version pinned to 0.6.4). It exposes twelve read-only tools: `health_check`, `list_topics`, `get_topic_info`, `sample_messages`, `analyze_bag`, `peek_bag_samples`, `list_participants`, `list_endpoints`, `detect_qos_mismatches`, `participant_events`, `peek_dds_samples`, `topic_metrics`.
|
|
8
8
|
- Skill `diagnose-dds-bus`: what to call, and in which order, when nodes do not talk, a topic gets no data, or a node crashed or restarts.
|
|
9
9
|
- Skill `inspect-ros2-robot`: listing topics, sampling messages and reading bags, including large arrays and simulation time.
|
|
10
10
|
|
|
@@ -44,6 +44,12 @@ The server runs with `TOPICFORGE_MODE=auto`: live ROS 2 when `ros2` is on PATH,
|
|
|
44
44
|
|
|
45
45
|
Without DDS, set `dds_backend` to `mock`, or use the standalone install `uvx topicforge`.
|
|
46
46
|
|
|
47
|
+
## Other clients (Agent Plugins)
|
|
48
|
+
|
|
49
|
+
This folder is also an [Agent Plugins](https://agent-plugins.org) 1.0.0 package, so Cursor and other compatible clients can load it: `plugin.json` is the manifest, `mcp.json` declares the server and `skills/` holds the two skills (shared with the Claude format above). The Claude files (`.claude-plugin/plugin.json`, `.mcp.json`) are left alone.
|
|
50
|
+
|
|
51
|
+
The Agent Plugins format has no user-configuration prompt, so `mcp.json` uses fixed defaults: `TOPICFORGE_MODE=auto`, `TOPICFORGE_DDS_BACKEND=cyclone`, `TOPICFORGE_DDS_DOMAIN_ID=0`. To change them, edit the `env` block of `mcp.json` in the installed copy (backend `mock` serves fixtures, `auto` picks the best available; domain 0 to 232, fixed at startup) and restart the server.
|
|
52
|
+
|
|
47
53
|
## Privacy
|
|
48
54
|
|
|
49
55
|
The server reads the local ROS 2 graph and the DDS discovery traffic on the machine and network it runs on, and returns the result to the Claude session. It makes no outbound network calls. Anonymous telemetry exists but is off by default and is not enabled by this plugin. See the main [README](https://github.com/yaniswav/TopicForge#telemetry) for the telemetry contract and [SECURITY.md](https://github.com/yaniswav/TopicForge/blob/main/SECURITY.md).
|
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
# After a release reaches PyPI, regenerate the lock: `cd plugin && uv lock`.
|
|
5
5
|
[project]
|
|
6
6
|
name = "topicforge-claude-plugin"
|
|
7
|
-
version = "0.6.
|
|
7
|
+
version = "0.6.4"
|
|
8
8
|
description = "Locked launch manifest for the TopicForge Claude plugin."
|
|
9
9
|
# cyclonedds ships wheels for CPython 3.10 to 3.13 only (checked on PyPI 2026-10-07).
|
|
10
10
|
requires-python = ">=3.10,<3.14"
|
|
11
11
|
dependencies = [
|
|
12
|
-
"topicforge==0.6.
|
|
12
|
+
"topicforge==0.6.4",
|
|
13
13
|
]
|
|
14
14
|
|
|
15
15
|
[tool.uv]
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "topicforge"
|
|
7
|
-
version = "0.6.
|
|
7
|
+
version = "0.6.4"
|
|
8
8
|
description = "ROS Topic Inspector & Bag Analyzer MCP server for AI agents"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -26,6 +26,12 @@ dependencies = [
|
|
|
26
26
|
"mcp>=1.0.0,<2",
|
|
27
27
|
"pydantic>=2.6",
|
|
28
28
|
"pyyaml>=6",
|
|
29
|
+
# Bag reader behind `peek_bag_samples`, core since 0.6.4 (it used to be the `[bags]`
|
|
30
|
+
# extra, which `uvx topicforge` and the plugin never selected). Pure Python
|
|
31
|
+
# (py3-none-any wheel); its dependencies apsw, lz4, numpy, zstandard and ruamel.yaml
|
|
32
|
+
# all ship wheels for linux-aarch64 and CPython 3.14 (checked on PyPI 2026-10-08), so
|
|
33
|
+
# no platform marker is needed.
|
|
34
|
+
"rosbags>=0.11.3",
|
|
29
35
|
# Cyclone DDS binding, core since 0.6.3 so that `uvx topicforge` (no extra) can join a
|
|
30
36
|
# DDS bus. The marker lists exactly the platforms where cyclonedds 11.0.1 ships wheels
|
|
31
37
|
# (checked on PyPI 2026-10-07): CPython 3.10-3.13 on Linux x86_64, Windows AMD64 and
|
|
@@ -40,11 +46,6 @@ dev = [
|
|
|
40
46
|
"pytest>=8.0",
|
|
41
47
|
"pytest-cov>=5.0",
|
|
42
48
|
"ruff>=0.4",
|
|
43
|
-
# rosbags (Apache-2.0, pure-Python) is a *dev* dependency so the
|
|
44
|
-
# bag-analysis I/O tests actually run in CI instead of self-skipping on
|
|
45
|
-
# `requires_rosbags`. It is NOT a runtime dependency: end users opt in
|
|
46
|
-
# via `topicforge[bags]`.
|
|
47
|
-
"rosbags>=0.11.3",
|
|
48
49
|
]
|
|
49
50
|
# DDS extras. Only bindings that are actually published on PyPI and used by
|
|
50
51
|
# a working adapter are declared here.
|
|
@@ -69,13 +70,9 @@ dds-cyclone = [
|
|
|
69
70
|
dds = [
|
|
70
71
|
"topicforge[dds-cyclone]",
|
|
71
72
|
]
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
|
|
75
|
-
# users without bag analysis don't need.
|
|
76
|
-
bags = [
|
|
77
|
-
"rosbags>=0.11.3",
|
|
78
|
-
]
|
|
73
|
+
# `[bags]` is kept as an alias since 0.6.4 (rosbags became a core dependency, so a plain
|
|
74
|
+
# install can run `peek_bag_samples`) so existing `topicforge[bags]` commands keep working.
|
|
75
|
+
bags = []
|
|
79
76
|
all = [
|
|
80
77
|
"topicforge[dds]",
|
|
81
78
|
]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Message timestamp extraction shared by the live ROS 2 and the bag readers.
|
|
2
|
+
|
|
3
|
+
Pure functions over a decoded payload (nested dicts and lists), tested without
|
|
4
|
+
ROS 2. The rule, in order:
|
|
5
|
+
|
|
6
|
+
1. a top-level `header.stamp` -> source `header`;
|
|
7
|
+
2. a few well-known messages that keep their time in the body -> source
|
|
8
|
+
`payload`: `rosgraph_msgs/Clock` (`clock`), `tf2_msgs/TFMessage`
|
|
9
|
+
(`transforms[0].header.stamp`, only when the list is not empty) and
|
|
10
|
+
`rcl_interfaces/Log` (`stamp`, recognised by its `level` and `msg` fields);
|
|
11
|
+
3. otherwise no stamp (`None`).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Any, Literal
|
|
17
|
+
|
|
18
|
+
StampKind = Literal["header", "payload"]
|
|
19
|
+
|
|
20
|
+
_NS_PER_S = 1_000_000_000
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def time_to_ns(stamp: object) -> int | None:
|
|
24
|
+
"""Nanoseconds of a `builtin_interfaces/Time` mapping, or `None` if it is not one."""
|
|
25
|
+
if not isinstance(stamp, dict):
|
|
26
|
+
return None
|
|
27
|
+
sec, nanosec = stamp.get("sec"), stamp.get("nanosec")
|
|
28
|
+
if not _is_int(sec) or not _is_int(nanosec):
|
|
29
|
+
return None
|
|
30
|
+
return int(sec) * _NS_PER_S + int(nanosec)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def payload_stamp(payload: dict[str, Any]) -> tuple[int, StampKind] | None:
|
|
34
|
+
"""The message's own time in nanoseconds and where it was found, or `None`."""
|
|
35
|
+
header = payload.get("header")
|
|
36
|
+
ns = time_to_ns(header.get("stamp")) if isinstance(header, dict) else None
|
|
37
|
+
if ns is not None:
|
|
38
|
+
return ns, "header"
|
|
39
|
+
ns = _body_stamp_ns(payload)
|
|
40
|
+
return None if ns is None else (ns, "payload")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _body_stamp_ns(payload: dict[str, Any]) -> int | None:
|
|
44
|
+
"""Time kept in the body of `Clock`, `TFMessage` or `Log`; `None` for any other shape."""
|
|
45
|
+
if "clock" in payload:
|
|
46
|
+
return time_to_ns(payload["clock"])
|
|
47
|
+
transforms = payload.get("transforms")
|
|
48
|
+
if isinstance(transforms, list):
|
|
49
|
+
first = transforms[0] if transforms else None
|
|
50
|
+
header = first.get("header") if isinstance(first, dict) else None
|
|
51
|
+
return time_to_ns(header.get("stamp")) if isinstance(header, dict) else None
|
|
52
|
+
if "level" in payload and "msg" in payload:
|
|
53
|
+
return time_to_ns(payload.get("stamp"))
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _is_int(value: object) -> bool:
|
|
58
|
+
return isinstance(value, int) and not isinstance(value, bool)
|
|
@@ -12,6 +12,7 @@ from typing import Any, Literal
|
|
|
12
12
|
|
|
13
13
|
import yaml
|
|
14
14
|
|
|
15
|
+
from topicforge.adapters.common.stamps import payload_stamp
|
|
15
16
|
from topicforge.constants import TRUNCATED_FIELDS_KEY
|
|
16
17
|
|
|
17
18
|
ECHO_DOCUMENT_SEPARATOR = "---"
|
|
@@ -20,7 +21,7 @@ ECHO_DOCUMENT_SEPARATOR = "---"
|
|
|
20
21
|
TRUNCATION_MARK = "..."
|
|
21
22
|
RAW_TEXT_KEY = "_raw_text"
|
|
22
23
|
|
|
23
|
-
StampSource = Literal["header", "none"]
|
|
24
|
+
StampSource = Literal["header", "payload", "none"]
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
@dataclass(frozen=True)
|
|
@@ -28,7 +29,8 @@ class EchoMessage:
|
|
|
28
29
|
"""One decoded echo document.
|
|
29
30
|
|
|
30
31
|
`timestamp_ns` is the top-level `header.stamp` (`stamp_source` `header`),
|
|
31
|
-
|
|
32
|
+
the time in the body of `Clock`, `TFMessage` or `Log` (`payload`), or 0
|
|
33
|
+
for a message without one (`none`).
|
|
32
34
|
"""
|
|
33
35
|
|
|
34
36
|
payload: dict[str, object]
|
|
@@ -97,20 +99,10 @@ def parse_echo_document(document: str, *, truncate_length: int | None = None) ->
|
|
|
97
99
|
payload = _normalize_mapping(data, "", truncate_length, cut)
|
|
98
100
|
if cut:
|
|
99
101
|
payload[TRUNCATED_FIELDS_KEY] = cut
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def _header_stamp_ns(payload: dict[str, object]) -> int | None:
|
|
105
|
-
"""Nanoseconds of a top-level `header.stamp`, whatever its value; `None` when absent."""
|
|
106
|
-
header = payload.get("header")
|
|
107
|
-
stamp = header.get("stamp") if isinstance(header, dict) else None
|
|
108
|
-
if not isinstance(stamp, dict):
|
|
109
|
-
return None
|
|
110
|
-
sec, nanosec = stamp.get("sec"), stamp.get("nanosec")
|
|
111
|
-
if not _is_int(sec) or not _is_int(nanosec):
|
|
112
|
-
return None
|
|
113
|
-
return int(sec) * 1_000_000_000 + int(nanosec)
|
|
102
|
+
found = payload_stamp(payload)
|
|
103
|
+
if found is None:
|
|
104
|
+
return EchoMessage(payload, 0, "none")
|
|
105
|
+
return EchoMessage(payload, found[0], found[1])
|
|
114
106
|
|
|
115
107
|
|
|
116
108
|
def _is_int(value: object) -> bool:
|
|
@@ -126,12 +126,15 @@ _MOCK_SAMPLES: dict[str, list[MessageSample]] = {
|
|
|
126
126
|
MessageSample(
|
|
127
127
|
topic="/tf",
|
|
128
128
|
message_type="tf2_msgs/msg/TFMessage",
|
|
129
|
-
timestamp_ns=
|
|
130
|
-
stamp_source="
|
|
129
|
+
timestamp_ns=_BASE_TS_NS + i * 100_000_000,
|
|
130
|
+
stamp_source="payload",
|
|
131
131
|
payload={
|
|
132
132
|
"transforms": [
|
|
133
|
-
{
|
|
134
|
-
|
|
133
|
+
{
|
|
134
|
+
"header": {"stamp": _stamp(_BASE_TS_NS + i * 100_000_000), "frame_id": a},
|
|
135
|
+
"child_frame_id": b,
|
|
136
|
+
}
|
|
137
|
+
for a, b in (("odom", "base_link"), ("base_link", "laser"))
|
|
135
138
|
]
|
|
136
139
|
},
|
|
137
140
|
)
|