xblick 0.5.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.
- xblick-0.5.0/.gitignore +9 -0
- xblick-0.5.0/AGENTS.md +123 -0
- xblick-0.5.0/CHANGELOG.md +391 -0
- xblick-0.5.0/CLAUDE.md +1 -0
- xblick-0.5.0/CMakeLists.txt +196 -0
- xblick-0.5.0/LICENSE +202 -0
- xblick-0.5.0/NOTICE +29 -0
- xblick-0.5.0/PKG-INFO +188 -0
- xblick-0.5.0/README.md +142 -0
- xblick-0.5.0/cmake/xblickConfig.cmake.in +24 -0
- xblick-0.5.0/cpp/include/xblick/camera.hpp +433 -0
- xblick-0.5.0/cpp/include/xblick/cuda_ring.hpp +161 -0
- xblick-0.5.0/cpp/include/xblick/image.hpp +66 -0
- xblick-0.5.0/cpp/include/xblick/msg.hpp +566 -0
- xblick-0.5.0/cpp/include/xblick/options.hpp +136 -0
- xblick-0.5.0/cpp/include/xblick/pairing.hpp +35 -0
- xblick-0.5.0/cpp/include/xblick/parameters.hpp +204 -0
- xblick-0.5.0/cpp/include/xblick/point_cloud_node.hpp +93 -0
- xblick-0.5.0/cpp/include/xblick/points.hpp +220 -0
- xblick-0.5.0/cpp/include/xblick/protocol.hpp +286 -0
- xblick-0.5.0/cpp/include/xblick/ray_cloud.hpp +40 -0
- xblick-0.5.0/cpp/include/xblick/rectify.hpp +76 -0
- xblick-0.5.0/cpp/include/xblick/ring.hpp +209 -0
- xblick-0.5.0/cpp/include/xblick/ros2_cdr_msgs.hpp +1406 -0
- xblick-0.5.0/cpp/include/xblick/rvl.hpp +43 -0
- xblick-0.5.0/cpp/include/xblick/version.hpp.in +8 -0
- xblick-0.5.0/cpp/points/camera_model.cpp +115 -0
- xblick-0.5.0/cpp/points/camera_model.hpp +62 -0
- xblick-0.5.0/cpp/points/kernels.cu +85 -0
- xblick-0.5.0/cpp/points/kernels.hpp +42 -0
- xblick-0.5.0/cpp/points/main.cpp +92 -0
- xblick-0.5.0/cpp/points/point_cloud_node.cpp +580 -0
- xblick-0.5.0/cpp/python/bindings.cpp +409 -0
- xblick-0.5.0/cpp/src/camera.cpp +1232 -0
- xblick-0.5.0/cpp/src/camera_info_file.cpp +314 -0
- xblick-0.5.0/cpp/src/cuda_ring.cpp +958 -0
- xblick-0.5.0/cpp/src/image.cpp +524 -0
- xblick-0.5.0/cpp/src/json.hpp +295 -0
- xblick-0.5.0/cpp/src/msg.cpp +959 -0
- xblick-0.5.0/cpp/src/options.cpp +554 -0
- xblick-0.5.0/cpp/src/pairing.cpp +95 -0
- xblick-0.5.0/cpp/src/parameters.cpp +1336 -0
- xblick-0.5.0/cpp/src/points.cpp +872 -0
- xblick-0.5.0/cpp/src/ray_cloud.cpp +69 -0
- xblick-0.5.0/cpp/src/rectify.cpp +252 -0
- xblick-0.5.0/cpp/src/ring.cpp +350 -0
- xblick-0.5.0/cpp/src/rvl.cpp +196 -0
- xblick-0.5.0/cpp/src/stream.cpp +800 -0
- xblick-0.5.0/cpp/src/stream.hpp +274 -0
- xblick-0.5.0/cpp/tests/test_latest.cpp +234 -0
- xblick-0.5.0/cpp/tests/test_lifetime.cpp +145 -0
- xblick-0.5.0/cpp/tests/test_messages.cpp +594 -0
- xblick-0.5.0/cpp/tests/test_options.cpp +148 -0
- xblick-0.5.0/cpp/tests/test_parameters.cpp +148 -0
- xblick-0.5.0/cpp/tests/test_partner.cpp +142 -0
- xblick-0.5.0/cpp/tests/test_rectify.cpp +147 -0
- xblick-0.5.0/cpp/tests/test_rings.cpp +226 -0
- xblick-0.5.0/cpp/tools/camera_tool.cpp +266 -0
- xblick-0.5.0/cpp/tools/probe_tool.cpp +195 -0
- xblick-0.5.0/cpp/tools/tools.hpp +146 -0
- xblick-0.5.0/docs/README.md +62 -0
- xblick-0.5.0/docs/concepts/gpu-frames.md +127 -0
- xblick-0.5.0/docs/concepts/latest-value.md +67 -0
- xblick-0.5.0/docs/concepts/protocol.md +53 -0
- xblick-0.5.0/docs/concepts/rings.md +108 -0
- xblick-0.5.0/docs/concepts/sessions.md +114 -0
- xblick-0.5.0/docs/concepts/streams-and-topics.md +86 -0
- xblick-0.5.0/docs/concepts/timestamps.md +112 -0
- xblick-0.5.0/docs/concepts/transports.md +250 -0
- xblick-0.5.0/docs/getting-started/first-node.md +120 -0
- xblick-0.5.0/docs/getting-started/first-stream.md +105 -0
- xblick-0.5.0/docs/getting-started/install.md +86 -0
- xblick-0.5.0/docs/guides/making-a-node-fast.md +128 -0
- xblick-0.5.0/docs/guides/pairing-streams.md +160 -0
- xblick-0.5.0/docs/guides/point-clouds.md +103 -0
- xblick-0.5.0/docs/guides/recording.md +275 -0
- xblick-0.5.0/docs/guides/sharing-a-gpu.md +70 -0
- xblick-0.5.0/docs/guides/tui.md +179 -0
- xblick-0.5.0/docs/guides/video.md +189 -0
- xblick-0.5.0/docs/guides/viewer.md +92 -0
- xblick-0.5.0/docs/guides/writing-nodes.md +172 -0
- xblick-0.5.0/docs/integrations/ros2.md +33 -0
- xblick-0.5.0/docs/project/alternatives.md +62 -0
- xblick-0.5.0/docs/project/development.md +124 -0
- xblick-0.5.0/docs/project/writing-docs.md +115 -0
- xblick-0.5.0/docs/reference/cli.md +216 -0
- xblick-0.5.0/docs/reference/cpp-api.md +154 -0
- xblick-0.5.0/docs/reference/glossary.md +32 -0
- xblick-0.5.0/docs/reference/nodes.md +177 -0
- xblick-0.5.0/docs/reference/parameters.md +306 -0
- xblick-0.5.0/docs/reference/python-api.md +223 -0
- xblick-0.5.0/docs/reference/topics.md +144 -0
- xblick-0.5.0/licenses/nanobind-LICENSE +26 -0
- xblick-0.5.0/licenses/robin-map-LICENSE +21 -0
- xblick-0.5.0/pyproject.toml +110 -0
- xblick-0.5.0/src/xblick/__init__.py +31 -0
- xblick-0.5.0/src/xblick/camera.py +1197 -0
- xblick-0.5.0/src/xblick/cdr.py +195 -0
- xblick-0.5.0/src/xblick/cli.py +780 -0
- xblick-0.5.0/src/xblick/codecs.py +137 -0
- xblick-0.5.0/src/xblick/controller.py +427 -0
- xblick-0.5.0/src/xblick/controller_manager.py +132 -0
- xblick-0.5.0/src/xblick/detections.py +101 -0
- xblick-0.5.0/src/xblick/diagnostics.py +60 -0
- xblick-0.5.0/src/xblick/encodings.py +97 -0
- xblick-0.5.0/src/xblick/gpu.py +139 -0
- xblick-0.5.0/src/xblick/history.py +256 -0
- xblick-0.5.0/src/xblick/logs.py +29 -0
- xblick-0.5.0/src/xblick/mps.py +256 -0
- xblick-0.5.0/src/xblick/msg.py +2022 -0
- xblick-0.5.0/src/xblick/msgdefs.py +23 -0
- xblick-0.5.0/src/xblick/node.py +199 -0
- xblick-0.5.0/src/xblick/parameters.py +787 -0
- xblick-0.5.0/src/xblick/paths.py +23 -0
- xblick-0.5.0/src/xblick/points.py +459 -0
- xblick-0.5.0/src/xblick/poses.py +200 -0
- xblick-0.5.0/src/xblick/recording.py +1622 -0
- xblick-0.5.0/src/xblick/rectify.py +42 -0
- xblick-0.5.0/src/xblick/rerun_bridge.py +892 -0
- xblick-0.5.0/src/xblick/rgbd.py +453 -0
- xblick-0.5.0/src/xblick/robot.py +103 -0
- xblick-0.5.0/src/xblick/session.py +213 -0
- xblick-0.5.0/src/xblick/sim.py +177 -0
- xblick-0.5.0/src/xblick/stamps.py +95 -0
- xblick-0.5.0/src/xblick/survey.py +404 -0
- xblick-0.5.0/src/xblick/tensors.py +50 -0
- xblick-0.5.0/src/xblick/topics.py +279 -0
- xblick-0.5.0/src/xblick/tui.py +1489 -0
- xblick-0.5.0/src/xblick/tui_graph.py +550 -0
- xblick-0.5.0/src/xblick/tui_parameters.py +427 -0
- xblick-0.5.0/src/xblick/video.py +555 -0
- xblick-0.5.0/tests/data/vision_msgs/BoundingBox2D.msg +13 -0
- xblick-0.5.0/tests/data/vision_msgs/BoundingBox3D.msg +10 -0
- xblick-0.5.0/tests/data/vision_msgs/Classification.msg +17 -0
- xblick-0.5.0/tests/data/vision_msgs/Detection2D.msg +23 -0
- xblick-0.5.0/tests/data/vision_msgs/Detection2DArray.msg +7 -0
- xblick-0.5.0/tests/data/vision_msgs/Detection3D.msg +23 -0
- xblick-0.5.0/tests/data/vision_msgs/Detection3DArray.msg +7 -0
- xblick-0.5.0/tests/data/vision_msgs/LICENSE +201 -0
- xblick-0.5.0/tests/data/vision_msgs/ObjectHypothesis.msg +12 -0
- xblick-0.5.0/tests/data/vision_msgs/ObjectHypothesisWithPose.msg +16 -0
- xblick-0.5.0/tests/data/vision_msgs/Point2D.msg +5 -0
- xblick-0.5.0/tests/data/vision_msgs/Pose2D.msg +4 -0
- xblick-0.5.0/tests/test_camera.py +666 -0
- xblick-0.5.0/tests/test_camera_info_file.py +89 -0
- xblick-0.5.0/tests/test_cdr.py +421 -0
- xblick-0.5.0/tests/test_cli_relay.py +72 -0
- xblick-0.5.0/tests/test_controller.py +205 -0
- xblick-0.5.0/tests/test_controller_manager_msgs.py +200 -0
- xblick-0.5.0/tests/test_cpp_interop.py +645 -0
- xblick-0.5.0/tests/test_images.py +122 -0
- xblick-0.5.0/tests/test_logs.py +74 -0
- xblick-0.5.0/tests/test_mps.py +204 -0
- xblick-0.5.0/tests/test_node.py +175 -0
- xblick-0.5.0/tests/test_parameters.py +376 -0
- xblick-0.5.0/tests/test_point_cloud_node.py +447 -0
- xblick-0.5.0/tests/test_points.py +440 -0
- xblick-0.5.0/tests/test_poses.py +99 -0
- xblick-0.5.0/tests/test_recording.py +402 -0
- xblick-0.5.0/tests/test_rectify.py +89 -0
- xblick-0.5.0/tests/test_rerun_bridge.py +1084 -0
- xblick-0.5.0/tests/test_rgbd.py +148 -0
- xblick-0.5.0/tests/test_rings.py +508 -0
- xblick-0.5.0/tests/test_robot_state.py +158 -0
- xblick-0.5.0/tests/test_session.py +173 -0
- xblick-0.5.0/tests/test_sim_recording.py +532 -0
- xblick-0.5.0/tests/test_stamps_rate.py +332 -0
- xblick-0.5.0/tests/test_tensors.py +38 -0
- xblick-0.5.0/tests/test_tui.py +668 -0
- xblick-0.5.0/tests/test_tui_graph.py +115 -0
- xblick-0.5.0/tests/test_tui_parameters.py +135 -0
- xblick-0.5.0/tests/test_tui_pty.py +198 -0
- xblick-0.5.0/tests/test_video.py +251 -0
- xblick-0.5.0/tests/test_vision_msgs.py +121 -0
- xblick-0.5.0/tools/build_wheels.sh +57 -0
- xblick-0.5.0/tools/check_references.sh +47 -0
- xblick-0.5.0/tools/gen_msgdefs.py +109 -0
- xblick-0.5.0/tools/install_zenoh.sh +19 -0
- xblick-0.5.0/tools/reference_allowlist.txt +7 -0
- xblick-0.5.0/tools/setup_python.sh +10 -0
- xblick-0.5.0/tools/tui_screenshots.py +306 -0
xblick-0.5.0/.gitignore
ADDED
xblick-0.5.0/AGENTS.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Guidance for coding agents in this repository. Human documentation: `README.md` (entry) and `docs/` (concepts,
|
|
4
|
+
guides, reference). Do not duplicate it here. Style rules for documentation: `docs/project/writing-docs.md`.
|
|
5
|
+
|
|
6
|
+
## What this is
|
|
7
|
+
|
|
8
|
+
Durchblick: camera streams and perception results between programs, over Zenoh, as ROS 2 message types (CDR),
|
|
9
|
+
without ROS. C++20 core in `cpp/` (Python bindings `xblick._core`), Python package `xblick` in `src/xblick/`
|
|
10
|
+
(command `xblick`). Each perception method is a node in its own repository (`xblick-<name>`) that depends on this
|
|
11
|
+
package. Robot integrations live in repositories of their own. The core never depends on a robot framework.
|
|
12
|
+
|
|
13
|
+
Names: every name in code, on the wire and on disk is `xblick` (since 0.5.0): Python package, distribution and
|
|
14
|
+
command, C++ namespace, include directory, CMake package and targets, C++ programs, `XBLICK_*` variables, Zenoh keys,
|
|
15
|
+
ring names, message types (`xblick_msgs`), directories. Only the prose name of the project is "Durchblick". A new
|
|
16
|
+
name follows this rule. Renaming a wire or on-disk name breaks every older program: only with a minor version.
|
|
17
|
+
|
|
18
|
+
## Skills
|
|
19
|
+
|
|
20
|
+
Project skills live in `.agents/skills/` (open Agent Skills format); `.claude/skills` links to it. Rules for writing
|
|
21
|
+
one: `.agents/skills/README.md`.
|
|
22
|
+
|
|
23
|
+
| Skill | For |
|
|
24
|
+
| --- | --- |
|
|
25
|
+
| `xblick-build-node` | a new vision node, or making a node faster or leaner |
|
|
26
|
+
| `xblick-run-nodes` | several nodes for an experiment or a demo, on one or more GPUs and PCs |
|
|
27
|
+
| `xblick-benchmark-nodes` | throughput, latency, GPU load and memory of nodes and setups |
|
|
28
|
+
| `xblick-develop-tui` | the terminal interface (`xblick`): tabs, dialogs, layout, its tests and screenshots |
|
|
29
|
+
|
|
30
|
+
- Before a task, look for a matching skill and follow it. Read its `SKILL.md` in full.
|
|
31
|
+
- After a complex task that is likely to recur and can be abstracted (no batch names, dates or results of one
|
|
32
|
+
experiment), propose a new skill or a fix of an existing one in two or three lines. Write it only after the user's
|
|
33
|
+
yes. Prefer improving a skill over adding one.
|
|
34
|
+
- Rules for every task stay in this file; a rule that must always hold belongs in a hook; a skill holds a procedure.
|
|
35
|
+
- Node repositories have no skills of their own. An agent working in one uses the skills of this repository.
|
|
36
|
+
|
|
37
|
+
## Commands
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
uv sync --all-extras # builds the C++ core (scikit-build-core, C++20); rebuilds when cpp/ changes
|
|
41
|
+
uv sync --reinstall-package xblick # force the rebuild
|
|
42
|
+
CUDA_VISIBLE_DEVICES=0 uv run pytest -q # ~5 min, ~633 tests (2026-11-13); CUDA tests skip without a GPU
|
|
43
|
+
uv run pytest -q tests/test_recording.py # one file
|
|
44
|
+
ruff check --line-length 120 src tests # lint; ruff is not a project dependency
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
C++ library, tools, point cloud node, C++ against Python: `docs/project/development.md`.
|
|
48
|
+
CI: `.github/workflows/ci.yml` (reference check, Python 3.10 and 3.12, C++ tests, interop, manylinux wheels).
|
|
49
|
+
PyPI files: `tools/build_wheels.sh <out> <work>` in quay.io/pypa/manylinux_2_28_x86_64 (CI job wheels, `.github/workflows/publish.yml`: tag v<version>, trusted publishing, environment pypi).
|
|
50
|
+
Reference check: `tools/check_references.sh` (no internal or robot-framework names outside its allowlist).
|
|
51
|
+
|
|
52
|
+
## Rules
|
|
53
|
+
|
|
54
|
+
- Fail fast. A wrong configuration, a missing input or a protocol mismatch raises with a message that says what to
|
|
55
|
+
do. Add no default that hides a misconfiguration.
|
|
56
|
+
- Protocol: a change of a Durchblick message's fields raises its version in `cpp/include/xblick/protocol.hpp`.
|
|
57
|
+
A ring layout change raises `kVersion` in `ring.hpp`. Either one also raises the package's minor version.
|
|
58
|
+
- Version: only in `CMakeLists.txt` (`project(xblick VERSION x.y.z)`). Every user-visible change: patch bump,
|
|
59
|
+
entry in `CHANGELOG.md`, status line in `README.md`. Release: tag `vx.y.z`, CI builds the wheels.
|
|
60
|
+
- Python and C++ stay equivalent. A feature of the camera, points or detections API exists in both.
|
|
61
|
+
`tests/test_cpp_interop.py` checks it.
|
|
62
|
+
- Documentation changes in the same commit as the code. New topic or option: update `docs/reference/`. History goes
|
|
63
|
+
to `CHANGELOG.md`, never into a page.
|
|
64
|
+
- A node stamps a result with the capture stamp of its input, never with the processing time.
|
|
65
|
+
- Code style: match the surrounding code. Docstrings say what, comments say why. 120 columns. No new dependency
|
|
66
|
+
without need.
|
|
67
|
+
|
|
68
|
+
## Traps
|
|
69
|
+
|
|
70
|
+
| Symptom | Cause and fix |
|
|
71
|
+
| --- | --- |
|
|
72
|
+
| Programs find nothing | A peer session with only `--connect` does not listen, and the router does not route between such sessions. Give no endpoints, or add `--listen tcp/127.0.0.1:0`, or use `--zenoh-mode client`. |
|
|
73
|
+
| A query finds nothing right after start | A new session needs time to learn of queryables. Queries repeat every `FIND_PERIOD`. Do not query once. |
|
|
74
|
+
| A loop over query replies hangs for minutes | Zenoh does not always finalize a query at its timeout. Never `for r in session.get(...)` or `liveliness().get`. Use `session.replies(session, selector, timeout, ...)` (C++ `xblick::replies`). |
|
|
75
|
+
| `Session.close()` never returns | Happens after a query Zenoh did not finalize. Open sessions with `open_session` or `open_from_args` (close waits at most 5 s). Wrap an own `zenoh.open` in `session.Session(...)`. |
|
|
76
|
+
| A subscriber gets JPEG on the publisher's host | `ImageSubscriber` defaults to `transport="compressed"`. Pass `transport="auto"`. |
|
|
77
|
+
| A test cannot open its own CUDA ring | The creating process cannot open a CUDA ring. Read it from a `spawn`ed child process. |
|
|
78
|
+
| Device frames arrive wrong or late | `publish()` of a device array needs `stream=` (for PyTorch `torch.cuda.current_stream()`). Without it the publisher waits for all GPU work of the process. |
|
|
79
|
+
| Nodes misbehave on a looped playback | `xblick play --loop` moves all stamps forward on each pass. Nodes rely on stamps that only grow. |
|
|
80
|
+
| A test or harness never ends | Zenoh threads keep a process alive. Close sessions. Run long checks in the background with `timeout`. Background processes ignore SIGINT: stop them by PID with SIGTERM. Never kill by name pattern. |
|
|
81
|
+
| Large writes fail | NFS. Build and write large outputs on a local disk. |
|
|
82
|
+
|
|
83
|
+
## Layout
|
|
84
|
+
|
|
85
|
+
| Path | Content |
|
|
86
|
+
| --- | --- |
|
|
87
|
+
| `cpp/include/xblick/protocol.hpp` | wire protocol: topics, message types, versions |
|
|
88
|
+
| `cpp/include/xblick/ring.hpp`, `cuda_ring.hpp` | host ring and CUDA ring |
|
|
89
|
+
| `cpp/include/xblick/camera.hpp`, `points.hpp`, `msg.hpp`, `parameters.hpp` | C++ API |
|
|
90
|
+
| `cpp/include/xblick/rectify.hpp`, `src/xblick/rectify.py` | rectification map from a CameraInfo (R, P), CPU remap; same in C++ and Python |
|
|
91
|
+
| `cpp/include/xblick/pairing.hpp` | `partner_of`: partner sample of a primary frame by stamp; Python's `PartnerHistory` calls it via `_core` |
|
|
92
|
+
| `cpp/points/` | point cloud node `xblick-points` (CUDA) |
|
|
93
|
+
| `cpp/python/bindings.cpp` | `xblick._core` |
|
|
94
|
+
| `src/xblick/camera.py`, `points.py`, `detections.py`, `session.py` | Python API: streams, sessions |
|
|
95
|
+
| `src/xblick/node.py`, `history.py`, `tensors.py`, `rgbd.py`, `parameters.py` | helpers for nodes |
|
|
96
|
+
| `src/xblick/cli.py`, `tui.py`, `tui_graph.py`, `rerun_bridge.py`, `recording.py`, `video.py` | tools |
|
|
97
|
+
| `src/xblick/robot.py`, `sim.py`, `controller.py`, `poses.py`, `mps.py` | robot state, simulation, controller link, poses, MPS |
|
|
98
|
+
| `tests/`, `cpp/tests/` | Python and C++ tests |
|
|
99
|
+
| `tools/` | `check_references.sh` (+ `reference_allowlist.txt`), `install_zenoh.sh`, `gen_msgdefs.py`, `setup_python.sh`, `tui_screenshots.py` |
|
|
100
|
+
| `.github/workflows/ci.yml` | CI |
|
|
101
|
+
| `docs/` | `getting-started/`, `concepts/`, `guides/`, `reference/`, `integrations/`, `project/` |
|
|
102
|
+
| `.agents/skills/` | agent skills; `.claude/skills` links here |
|
|
103
|
+
|
|
104
|
+
## Defaults
|
|
105
|
+
|
|
106
|
+
- Router: `zenohd` 1.10 on `127.0.0.1:7447`, one per PC. Start: `zenohd --no-multicast-scouting -l tcp/0.0.0.0:7447`.
|
|
107
|
+
- Stream name: `<namespace>/<camera>/<stream>`. Node results: `<camera>/<node>/<name>`. Diagnostics:
|
|
108
|
+
`<camera>/diagnostics`.
|
|
109
|
+
- Transports: `auto`, `compressed`, `raw`, `shm`, `cuda`. Nodes and the CLI use `auto`.
|
|
110
|
+
- Image ring: 32 slots. Point cloud ring: 4 slots.
|
|
111
|
+
- Test without a camera: `xblick test-camera` publishes `robot/test_camera/color` (640 x 480, 15 Hz).
|
|
112
|
+
`xblick play <file.mcap> --loop` replays a recording.
|
|
113
|
+
- Inspect: `xblick list`, `xblick hz <stream>`, `xblick diagnostics <namespace>`, `xblick rerun`, `xblick`
|
|
114
|
+
(terminal interface).
|
|
115
|
+
- Two programs without a router (tests): one with `--listen tcp/127.0.0.1:<port>`, the other with
|
|
116
|
+
`--connect tcp/127.0.0.1:<port>`.
|
|
117
|
+
|
|
118
|
+
## Node repositories
|
|
119
|
+
|
|
120
|
+
Same pattern in each: a uv project pinned to a version of `xblick` (`pyproject.toml`, `uv.lock`), `tools/setup.sh`
|
|
121
|
+
for `.venv` and checkpoints, a console script `xblick-<name>`, outputs below `<camera>/<node>/`, diagnostics on
|
|
122
|
+
`<camera>/diagnostics`, its own `AGENTS.md`. A change here that nodes need: release it, then raise their pin and
|
|
123
|
+
run `uv lock`.
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
# Versions
|
|
2
|
+
|
|
3
|
+
*Durchblick's [overview](README.md).*
|
|
4
|
+
|
|
5
|
+
Readers and publishers of one setup run the same minor version (0.3.x): the info record's version changed with 0.3.0.
|
|
6
|
+
Within 0.3.x, 0.3.59 changed the rings' keys and the info record's version (3): every program of a setup runs 0.3.59
|
|
7
|
+
or newer. 0.4.0 to 0.4.3 speak the protocol of 0.3.59 to 0.3.68: their programs and those of 0.3.68 work together.
|
|
8
|
+
0.5.0 renamed every name on the wire and on disk: programs of 0.5 do not work with programs of 0.3 or 0.4.
|
|
9
|
+
|
|
10
|
+
* **0.5.0** Every name in code, on the wire and on disk is `xblick`. Only the prose name of the project stays
|
|
11
|
+
Durchblick. **Breaking for everything:** programs of 0.5 do not talk to programs of 0.3 or 0.4. Their own message
|
|
12
|
+
types, info records, ring names and keys differ, and a 0.5 reader refuses a 0.4 info record (its version is 4
|
|
13
|
+
now). Only samples of ROS types (for example `image_raw`, `image_raw/compressed`, `camera_info`) keep their bytes.
|
|
14
|
+
Every program and node of a setup must run 0.5. Recordings of 0.4 still play, but their channels lack the info
|
|
15
|
+
record of `xblick_info`, so `play` serves their streams as plain topics, as it does for other MCAP files. The
|
|
16
|
+
repository is `github.com/xblick/xblick`. Node repositories are `xblick-<name>`, the relay is `xblick-relay`.
|
|
17
|
+
The versions of the message types (`@1`) and of the rings (`kVersion`) stay, since their fields and layouts did not
|
|
18
|
+
change. The ring magics `DBLKRING` and `DBLKCUDA` stay as well.
|
|
19
|
+
|
|
20
|
+
| What | Old (0.4) | New (0.5) |
|
|
21
|
+
| --- | --- | --- |
|
|
22
|
+
| Python package, extension | `import blick`, `blick._core` | `import xblick`, `xblick._core` |
|
|
23
|
+
| Distribution, extras | `xblick`, `xblick[relay]` with `blick-relay` 0.3 | unchanged, `xblick[relay]` with `xblick-relay` 0.4 |
|
|
24
|
+
| Command | `blick` (`blick list`, `blick relay`, `blick mps`, ...) | `xblick` (`xblick list`, `xblick relay`, ...) |
|
|
25
|
+
| Logger | `blick` | `xblick` |
|
|
26
|
+
| Terminal interface class | `tui.DurchblickApp` | `tui.XblickApp` |
|
|
27
|
+
| Relay package and program | `blick_relay`, `durchblick-relay` | `xblick_relay`, `xblick-relay` |
|
|
28
|
+
| C++ namespace | `durchblick::` | `xblick::` |
|
|
29
|
+
| C++ headers | `#include <durchblick/camera.hpp>`, `<durchblick/version.hpp>` | `#include <xblick/camera.hpp>`, `<xblick/version.hpp>` |
|
|
30
|
+
| Version macros | `DURCHBLICK_VERSION`, `DURCHBLICK_VERSION_MAJOR/MINOR/PATCH` | `XBLICK_VERSION`, `XBLICK_VERSION_MAJOR/MINOR/PATCH` |
|
|
31
|
+
| CMake package | `find_package(durchblick 0.4)`, `lib/cmake/durchblick` | `find_package(xblick 0.5)`, `lib/cmake/xblick` |
|
|
32
|
+
| CMake targets | `durchblick::core`, `durchblick::durchblick`, `durchblick::points` | `xblick::core`, `xblick::xblick`, `xblick::points` |
|
|
33
|
+
| CMake options and variables | `DURCHBLICK_CAMERA`, `DURCHBLICK_POINTS`, `DURCHBLICK_CUDA_INCLUDE_DIR`, `durchblick_points_FOUND` | `XBLICK_CAMERA`, `XBLICK_POINTS`, `XBLICK_CUDA_INCLUDE_DIR`, `xblick_points_FOUND` |
|
|
34
|
+
| Libraries | `libdurchblick_core.a`, `libdurchblick.a`, `libdurchblick_points.a` | `libxblick_core.a`, `libxblick.a`, `libxblick_points.a` |
|
|
35
|
+
| Programs | `durchblick-points`, `durchblick-cpp-camera`, `durchblick-cpp-probe` | `xblick-points`, `xblick-cpp-camera`, `xblick-cpp-probe` |
|
|
36
|
+
| Test variables | `DURCHBLICK_CPP_BUILD`, `DURCHBLICK_TUI_SCREENS` | `XBLICK_CPP_BUILD`, `XBLICK_TUI_SCREENS` |
|
|
37
|
+
| Message types | `durchblick_msgs/msg/...`, `durchblick_msgs/srv/...` | `xblick_msgs/msg/...`, `xblick_msgs/srv/...` |
|
|
38
|
+
| Info record (`<base>/info`, detections' info) | `{"v":3, ..., "durchblick":"<version>"}` | `{"v":4, ..., "xblick":"<version>"}` |
|
|
39
|
+
| Shared-memory rings | `/dev/shm/durchblick-<16 hex>` | `/dev/shm/xblick-<16 hex>` |
|
|
40
|
+
| CUDA ring handshake socket | `\0durchblick-<16 hex>` | `\0xblick-<16 hex>` |
|
|
41
|
+
| Record and play node | `durchblick/<name>/control`, `durchblick/<name>/status`, parameters of `durchblick/<name>` | `xblick/<name>/control`, `xblick/<name>/status`, parameters of `xblick/<name>` |
|
|
42
|
+
| MPS daemon | token `durchblick/mps/<name>`, `.../status` `{"v":1, ..., "durchblick":...}`, line `durchblick mps: ready` | token `xblick/mps/<name>`, `.../status` `{"v":2, ..., "xblick":...}`, line `xblick mps: ready` |
|
|
43
|
+
| MCAP files | library `durchblick <version>`, channel metadata `durchblick_info`, record `durchblick_recording` | library `xblick <version>`, `xblick_info`, `xblick_recording` |
|
|
44
|
+
| Rerun application id | `durchblick` | `xblick` |
|
|
45
|
+
| Presets of the terminal interface | `$XDG_CONFIG_HOME/durchblick/rerun_presets.json` | `$XDG_CONFIG_HOME/xblick/rerun_presets.json` |
|
|
46
|
+
| Test pattern's JPEG comment | `durchblick-test-jpeg <seq>` | `xblick-test-jpeg <seq>` |
|
|
47
|
+
| Agent skills | `durchblick-build-node`, `durchblick-run-nodes`, `durchblick-benchmark-nodes`, `durchblick-develop-tui` | `xblick-build-node`, `xblick-run-nodes`, `xblick-benchmark-nodes`, `xblick-develop-tui` |
|
|
48
|
+
|
|
49
|
+
* **0.4.3** The first version on PyPI. The distribution is `xblick` there (`pip install xblick`; PyPI does not allow
|
|
50
|
+
the name `blick`); the module `blick` and the command `blick` keep their names. The extra `xblick[relay]` installs
|
|
51
|
+
the relay as the package `blick-relay` (PyPI, 0.3.1 or newer of its 0.3 line), which `blick relay` starts. The sdist
|
|
52
|
+
no longer carries the wheels of a build that wrote to `dist/` in the checkout, and `tools/build_wheels.sh` fails if
|
|
53
|
+
it does. The library is that of 0.4.2.
|
|
54
|
+
* **0.4.2** `blick relay [arguments]` starts the relay program `durchblick-relay` with the arguments as they stand
|
|
55
|
+
(it replaces the `blick` process). It looks for the program in the installed package `blick-relay` first, then on
|
|
56
|
+
`PATH`, and fails with a message that names `pip install "xblick[relay]"` and the CMake build when neither has it.
|
|
57
|
+
The extra `xblick[relay]` installs `blick-relay` 0.3.1 or a newer 0.3 version, a wheel with the program and
|
|
58
|
+
`libzenohc.so`. The first version on PyPI: wheels for CPython 3.10 to 3.13 (manylinux_2_28, x86-64) and the sdist,
|
|
59
|
+
built by `tools/build_wheels.sh` and published by `.github/workflows/publish.yml`. The wheels carry the licences of
|
|
60
|
+
nanobind and robin-map, which `blick._core` contains (`NOTICE`). Compatible with 0.4.1, 0.4.0 and 0.3.68.
|
|
61
|
+
* **0.4.1** Fix: a subscriber with `transport="auto"` that moved to another transport could hand out a frame
|
|
62
|
+
twice, or an older one after a newer. Such a move happens from the network to a relay's ring and back. The
|
|
63
|
+
subscriber compared the stamps of the two routes only until the new one delivered its first sample. It also
|
|
64
|
+
accepted the old route's samples until that route's subscriber was undeclared. A relay's ring lags the network, so
|
|
65
|
+
its first frames are ones that the network handed out already. Now the subscriber leaves out the old route's
|
|
66
|
+
samples from the new route's first sample on, also those that Zenoh delivers while the old subscriber is
|
|
67
|
+
undeclared. It compares the new route's samples with the newest frame handed out until one is newer, for at most
|
|
68
|
+
2 s, and leaves out those that are not newer. On the new route, a stamp more than 2 s behind the newest one is a
|
|
69
|
+
restart of the stream, and the subscriber hands it out at once. The old route's restarted frames come again by
|
|
70
|
+
the new route. During a move the stamps handed out only grow, except after a restart of the stream. What remains
|
|
71
|
+
is listed under "Status and limits" in `docs/concepts/transports.md`: copies more than 2 s behind count as a
|
|
72
|
+
restart, a new route that pauses for more than 2 s hands out older frames again, frames with one stamp pause for up
|
|
73
|
+
to 2 s, and a frame can be lost when the new transport is ahead. Python and C++ behave alike, with the window as an
|
|
74
|
+
integer of nanoseconds in both. The C++ rule has a test of its own (`cpp/tests/test_latest.cpp`).
|
|
75
|
+
`test_auto_moves_to_a_ring_that_appears_and_back` failed this way once on a 2-core CI runner. With the ring one or
|
|
76
|
+
two frames behind the network it failed every time. Compatible with 0.4.0 and 0.3.68.
|
|
77
|
+
* **0.4.0** The Python package and its command are `blick`: the distribution `blick` (`pip install blick`), `import
|
|
78
|
+
blick` (was `import durchblick`), the extension `blick._core`, the command `blick` (the command `durchblick` is
|
|
79
|
+
gone). **Breaking for Python code and scripts:** replace `durchblick` by `blick` in imports, dependencies and
|
|
80
|
+
command lines. The logger is `blick`. Unchanged: the protocol and every name on the wire or on disk (Zenoh keys,
|
|
81
|
+
ring names, `durchblick_msgs`, the info record's `durchblick` version field, the recorder's node namespace
|
|
82
|
+
`durchblick`, the MCAP library name, the presets directory), the `DURCHBLICK_*` variables, and the C++ side
|
|
83
|
+
(namespace, include directory, CMake package and targets, tools). `blick mps` still prints `durchblick mps: ready`.
|
|
84
|
+
The C++ package version is 0.4.0, so `find_package(durchblick 0.3...)` no longer accepts it (the version file
|
|
85
|
+
requires the same minor version): ask for 0.4. The repository has no robot framework in it: the ArmarX integration
|
|
86
|
+
lives in durchblick_armarx. Examples use the namespace `robot` (`blick test-camera` publishes
|
|
87
|
+
`robot/test_camera/color`). CI on GitHub Actions with a check that no internal
|
|
88
|
+
names enter the repository (`tools/check_references.sh`). Licence: Apache-2.0 (`LICENSE`, `NOTICE`).
|
|
89
|
+
* **0.3.68** Pairing by stamp without a wasted frame, also with an explicit offset and in C++: from a steady partner
|
|
90
|
+
(the steps of its kept stamps within a tenth of their median), a nearest sample at most a quarter of the smallest step
|
|
91
|
+
before the primary stamp pairs at once, with the default offset and with an explicit `max_offset_ns`. A partner that
|
|
92
|
+
skips camera frames (a node's depth of some frames) is not steady and waits, as 0.3.65 did: its next sample may be the
|
|
93
|
+
primary frame's own. 0.3.66 judged by the median step alone, and it kept waiting for the next sample (a whole period)
|
|
94
|
+
with an explicit offset. durchblick-objectpoints (`--max-offset-ms 5`) on Fast-FoundationStereo depth stamped 1.7 ms
|
|
95
|
+
before the colour frame waited 67 ms per frame at 15 Hz. The rule is C++ now: `partner_of` and `partner_period_ns` in
|
|
96
|
+
`durchblick/pairing.hpp` (`durchblick::core`, in Python `_core.partner_of`). `PartnerHistory` and the point cloud node
|
|
97
|
+
call it, so both pair alike. Before, the point cloud node waited for a later colour sample whenever the nearest one
|
|
98
|
+
lay before the depth stamp, and it compared with the last sample to arrive instead of the newest stamp. Until two
|
|
99
|
+
samples give a period, a partner waits as before. The point cloud node refuses a negative `max_offset`. Documentation:
|
|
100
|
+
Fast-FoundationStereo stamps its depth and disparity with the left frame's stamp, no longer with the mean of the
|
|
101
|
+
pair's stamps, so depth and left image pair by equal stamps. ArmarX integration: see durchblick_armarx.
|
|
102
|
+
Fix: the C++ `CameraPublisher` and `PointCloudPublisher` undeclare their `<base>/info` (and rays) queryables and
|
|
103
|
+
their liveliness token first when destroyed. Before, a query answered during the destruction read freed members, because a CUDA ring's server takes up
|
|
104
|
+
to 200 ms to stop. durchblick-relay 0.2.1 stopped with `std::length_error`. Compatible with 0.3.67.
|
|
105
|
+
* **0.3.67** ArmarX integration: see durchblick_armarx. Durchblick itself is unchanged; compatible with 0.3.66.
|
|
106
|
+
* **0.3.66** Pairing by stamp without a wasted frame: `PartnerHistory.partner_of` with the measured offset (the
|
|
107
|
+
default) takes a nearest partner sample that lies just before the primary stamp at once, because the partner's next
|
|
108
|
+
sample comes at least half its period later and cannot be nearer. It waited for that next sample, a whole period.
|
|
109
|
+
On a stereo pair whose right camera stamps 0.24 ms before the left one, durchblick-fast-foundationstereo's depth came
|
|
110
|
+
one frame (67 ms at 15 Hz) later than needed. An explicit `max_offset_ns` keeps the old rule. Compatible with 0.3.65.
|
|
111
|
+
* **0.3.65** Rectification in C++ and Python: `durchblick/rectify.hpp` (`rectify_map`, `remap`,
|
|
112
|
+
`rectified_info`, in `durchblick::core` without the messages) and `durchblick.rectify` (`rectify_map`,
|
|
113
|
+
`rectified_info`) compute the map of OpenCV's `initUndistortRectifyMap` from a CameraInfo: its R (a stereo pair's
|
|
114
|
+
rotation), its P (with a baseline), plumb_bob, rational_polynomial and equidistant. The map equals OpenCV's within
|
|
115
|
+
0.002 px. `remap` interpolates 8-bit images bilinearly and takes the nearest pixel of 16-bit ones. For nodes that
|
|
116
|
+
rectify, such as durchblick-rectify. ArmarX integration: see durchblick_armarx. Compatible with 0.3.64.
|
|
117
|
+
* **0.3.64** Compressed point clouds in C++: `CloudPublisherOptions::rays` (`CloudRays`), `set_rays()`,
|
|
118
|
+
`Transport::Compressed` and `CloudSubscriberOptions::remote` for clouds, as in Python. The point cloud node
|
|
119
|
+
(`durchblick-points`) passes the rays of its depth camera, so a reader on another PC takes
|
|
120
|
+
its cloud as ranges and colours, about a twentieth of the whole cloud (`--range-step`, `--jpeg-quality`). C++ and
|
|
121
|
+
Python read each other's compressed clouds bit for bit. Compatible with 0.3.63.
|
|
122
|
+
* **0.3.63** Compressed point clouds: a `PointCloudPublisher` with `rays` (the direction of each point's ray)
|
|
123
|
+
offers `cloud/compressed`, the cloud as the ranges of its points (16 bit, RVL) and their colours (JPEG of quality
|
|
124
|
+
95, or PNG). The rays go once to each subscriber (`cloud/rays`). A Kinect cloud of 6.19 MB becomes 0.33 MB, in
|
|
125
|
+
5 ms. `PointCloudSubscriber` with `auto` takes it from another PC where the publisher offers it (`remote`).
|
|
126
|
+
Python only (C++: 0.3.64). Compatible with 0.3.62, whose readers take `raw`.
|
|
127
|
+
* **0.3.62** RVL for 16-bit depth on `image_raw/compressed`: `codec="rvl"` (C++ `kRvl`, `rvl.hpp`), lossless, on
|
|
128
|
+
Kinect depth nine times as fast to encode as PNG (1.3 against 12.3 ms) and a fifth smaller. The message is ROS's
|
|
129
|
+
`compressedDepth` with the format `rvl` (`16UC1; compressedDepth rvl`). Depth from a video (`durchblick play`)
|
|
130
|
+
goes out as RVL. PNG stays for label images. **A reader older than 0.3.62 stops with an error on an RVL
|
|
131
|
+
stream:** update the readers before the cameras.
|
|
132
|
+
* **0.3.61** ArmarX integration: see durchblick_armarx. The libraries are those of 0.3.60.
|
|
133
|
+
* **0.3.60** A subscriber with `auto` asks the relay of its host for a stream: it holds the liveliness token
|
|
134
|
+
`<base>/want/<host>/<id>` while the stream has no source with a ring on its host (`topics.want`,
|
|
135
|
+
`topics.wanted_on`, `topics.wanted_stream`; C++ `protocol::kWant`). durchblick-relay 0.2.0 runs once per PC and
|
|
136
|
+
relays the streams wanted there, without being told which. Compatible with 0.3.59: a publisher or relay of 0.3.59
|
|
137
|
+
ignores the token.
|
|
138
|
+
* **0.3.59** The rings' keys name the ring's place, and a subscriber keeps its transport up to date. **Protocol
|
|
139
|
+
change: info record version 3; publishers and subscribers of 0.3.58 and older do not work with it.**
|
|
140
|
+
Keys: `<base>/image_raw/shm/<host>` and `<base>/image_raw/cuda/<host>/<gpu>` (`cloud/...` alike), `<host>` the
|
|
141
|
+
host's id and `<gpu>` the GPU's UUID (`topics.shm(base, host)`, `topics.cuda(base, host, gpu)`,
|
|
142
|
+
`topics.ring_place`; C++ `protocol::kAnyGpu`): a descriptor reaches only readers on the ring's host, and
|
|
143
|
+
publishers on several hosts (a source and relays) offer rings of one stream side by side. Every stream publisher
|
|
144
|
+
holds a liveliness token on `<base>/info/<host>/<id>`; a subscriber with `auto` chooses again whenever a publisher of its
|
|
145
|
+
stream comes or goes, so the order in which programs start does not matter (a relay started after its readers is
|
|
146
|
+
taken; when it stops they go back to the network). While it changes its transport a subscriber reads both routes
|
|
147
|
+
until the new one's first frame (at most 2 s) and hands out each frame once, by its stamp. A publisher with
|
|
148
|
+
`cuda_device` needs the CUDA driver when it is made. `blick`'s key tree shows a ring's place by 8 characters.
|
|
149
|
+
* **0.3.58** A reader on another GPU of the publisher's host gets its frames on its own GPU, copied from GPU to GPU:
|
|
150
|
+
`CudaRingReader(endpoint, target_device)` copies out of the ring with `cuMemcpyPeerAsync`, over PCIe where the two
|
|
151
|
+
devices reach each other (`direct`), else through host memory. `auto` with a `device` takes the CUDA ring of another
|
|
152
|
+
GPU when its process sees that GPU (`cuda (copied from the publisher's GPU ...)`), else the host ring as before;
|
|
153
|
+
`local="cuda"` with a `device` delivers on that device too. A reader that asks for views (`copy=False`) gets copies
|
|
154
|
+
from a ring on another GPU. `cuda_device_ordinal(uuid)`. 1080p `rgb8`, two RTX PRO 6000: 0.16 ms a frame, against
|
|
155
|
+
0.53 ms through the host ring with an upload; the reader's process holds a CUDA context on the publisher's GPU
|
|
156
|
+
(552 MiB there).
|
|
157
|
+
* **0.3.57** `durchblick::Options` (C++, `durchblick/options.hpp`): a program's options defined once (name, default and
|
|
158
|
+
type, help, live), parsed from the command line (unknown, twice, out of range or choices: the reason and the usage,
|
|
159
|
+
exit 2; `--help`), and declared as the node's parameters, read-only but the live ones with their writers; a value
|
|
160
|
+
given at the start with `--param` or a params file becomes the option's. With the session's options
|
|
161
|
+
(`--connect`, `--listen`, `--zenoh-mode`, `--zenoh-config`) and the parameters' own. `ParameterOptions`' members
|
|
162
|
+
have initializers (a designated initializer may leave any out without `-Wmissing-field-initializers`). The relay,
|
|
163
|
+
the Azure Kinect camera and body tracker and the rc_visard node use it.
|
|
164
|
+
* **0.3.56** A node's parameter services answer in a daemon thread of their own (queries from Zenoh's channels,
|
|
165
|
+
looked for every 10 ms), no longer in zenoh-python's callback thread, which is no daemon: a node that ended with an
|
|
166
|
+
exception before `Parameters.close()` (an interrupt while its model loads, now that nodes offer their parameters
|
|
167
|
+
from the start) waited for that thread for good. `close()` may be called twice.
|
|
168
|
+
* **0.3.55** `Parameters.declare_options(parser, args, live=...)` declares every command-line option of a node as a
|
|
169
|
+
parameter: read-only (at the next start) except the live ones, the help as description, `--param` and
|
|
170
|
+
`--params-file` written back into `args`. The nodes use it, so that the terminal interface shows how each runs.
|
|
171
|
+
* **0.3.54** The terminal interface:
|
|
172
|
+
* The Graph tab draws the sessions as boxes in columns and the data between them as arrows with labels
|
|
173
|
+
(`tui_graph.py`, a small layered layout). The arrow keys or a click select a session and colour its edges. The
|
|
174
|
+
mouse over an edge or a box shows its keys and how each is taken. The boxes show each session's diagnostics.
|
|
175
|
+
* The Topics tab is a key tree. Enter folds a prefix. A prefix's publishers and subscribers stand once on the
|
|
176
|
+
highest row below which every key has the same, in new columns "published by" and "subscribed by". Sessions
|
|
177
|
+
have short names there.
|
|
178
|
+
* The connect dialog opens on the endpoints with the cursor in the address field. Typing into a field selects its
|
|
179
|
+
way. The interface opens the dialog by itself when it starts on this PC's router and finds nothing there.
|
|
180
|
+
* `r` and the Rerun tab's "Look again" search the network at once. The tab says when it last searched.
|
|
181
|
+
* `session.endpoint` gives an address without a port 7447 (`tcp/robot-pc` is `tcp/robot-pc:7447`), as
|
|
182
|
+
`robot-pc` was already. It refuses an IPv6 address without brackets and an empty endpoint.
|
|
183
|
+
* The Parameters tab: one line per parameter (name, field, the descriptor's text; under the field below 120
|
|
184
|
+
columns). A node opens when highlighted. ↑/↓ step a number by its range's step and set it; Esc takes an edit
|
|
185
|
+
back; an edited field not yet set is marked; "set" confirms a change. A bool is a one-line checkbox.
|
|
186
|
+
* `tools/tui_screenshots.py` makes the screenshots of docs/guides/tui.md from a made-up network.
|
|
187
|
+
* **0.3.53** ArmarX integration: see durchblick_armarx.
|
|
188
|
+
* **0.3.52** C++ `auto` on another host refuses a source that does not offer the remote form (`compressed` by
|
|
189
|
+
default) with ProtocolError, as Python's `ImageSubscriber` does, instead of waiting for frames that never come (the
|
|
190
|
+
Jetson's ecam24 node in raw mode offers only `raw`). Relays' records are left out of that check.
|
|
191
|
+
* **0.3.51** `find_streams` (`durchblick list`, `rerun`, the recorder, the terminal interface) keeps one record per
|
|
192
|
+
stream, the source's, with its relays' hosts in `relayed_by` (`durchblick list`: "relayed on <host>"); before, a
|
|
193
|
+
relay's record (only rings, no `compressed`) could stand for the stream.
|
|
194
|
+
* **0.3.50** Queries take every reply (`replies()`, C++ and Python, without Zenoh's consolidation): with the default
|
|
195
|
+
consolidation two publishers of one stream (a source and its relay) answered `<base>/info` as one, so a subscriber
|
|
196
|
+
or the relay saw one of them at random; a caller that counts the answers of a service (one controller manager per
|
|
197
|
+
robot) saw duplicates merged.
|
|
198
|
+
* **0.3.49** Relays (durchblick-relay): the C++ `PublisherOptions` take `transports` (the representations offered;
|
|
199
|
+
a relay offers only its host's rings, `shm`/`cuda`) and `relayed_from` (in the info record, null otherwise), and
|
|
200
|
+
`CameraPublisher::publish(image, Origin)` republishes a frame with the source's header, `seq` and `source`. C++
|
|
201
|
+
`stream_infos` (info records read) and `parse_frame_attachment`. `camera_info` is declared only with a CameraInfo.
|
|
202
|
+
Python's `auto` on a third host no longer refuses a stream whose relay record has no `compressed` form.
|
|
203
|
+
* **0.3.48** The terminal interface's Rerun tab lists pose topics (`<camera>/tf`, `<camera>/<node>/tf`, not
|
|
204
|
+
`<robot>/tf`) for `--trajectory`; presets keep them.
|
|
205
|
+
* **0.3.47** `durchblick rerun --trajectory KEY`: poses in the world from any TFMessage topic (durchblick-vslam's
|
|
206
|
+
`<camera>/vslam/tf`, `<camera>/tf` to compare) as a path with axes at the newest pose, at `trajectories/<key>`.
|
|
207
|
+
* **0.3.46** A robot's controller manager on Zenoh, as ROS 2's (`<robot>/controller_manager`: `list_controllers`,
|
|
208
|
+
`switch_controller`, `unload_controller` with ros2_control Jazzy's controller_manager_msgs, checked byte for byte
|
|
209
|
+
against rosbags; and Durchblick's `create_controller` from a type and a JSON config, `get_controller_config`,
|
|
210
|
+
`get_config_file`), served by the robot's software or a bridge to it (ArmarX
|
|
211
|
+
integration: see durchblick_armarx).
|
|
212
|
+
Its STRICT switch refuses to activate a controller on a joint another active controller holds unless that one is
|
|
213
|
+
deactivated in the same switch. Python: `durchblick.controller_manager` (`ControllerManager`, `torque_step`),
|
|
214
|
+
`durchblick.controller.JointPositionSubscriber` for a joint position controller. A
|
|
215
|
+
controller's `torque_step` service (durchblick_msgs/srv/TorqueStep). The shared `ros2_cdr_msgs.hpp` is version 2
|
|
216
|
+
(`u16`, TorqueStep).
|
|
217
|
+
* **0.3.45** The messages a robot's controller speaks (Time, Header, TFMessage and its geometry, DiagnosticArray,
|
|
218
|
+
JointState, WrenchStamped, MultiDOFJointState, RobotState, Float64MultiArray, the rcl_interfaces parameter messages)
|
|
219
|
+
and the CDR writer and reader moved from `msg.hpp`/`msg.cpp`/`parameters.cpp` into
|
|
220
|
+
`cpp/include/durchblick/ros2_cdr_msgs.hpp`, header only, in the namespace `ros2_cdr::v1` and named in `durchblick::`
|
|
221
|
+
as before (no other change in C++ or Python; the bytes are the same, `test_messages`). A controller may
|
|
222
|
+
carry a copy of the file without depending on Durchblick. A change of it raises `ROS2_CDR_MSGS_VERSION` (and the
|
|
223
|
+
inline namespace with it). ArmarX integration: see durchblick_armarx.
|
|
224
|
+
* **0.3.44** Paired frames taken together, and 32 ring slots (on a robot, Boxer asked for a colour frame's
|
|
225
|
+
depth after its 2D stage, about 120 ms later; the camera had reused the depth's ring slot by then, so no frame had
|
|
226
|
+
depth). Image rings keep 32 frames by default (`camera.DEFAULT_SLOTS`, C++ `PublisherOptions::slots`): a reader on
|
|
227
|
+
the same host has 31 frame periods, about a second at 30 Hz, instead of 3. Point cloud rings keep 4 (large, and
|
|
228
|
+
nothing pairs them). A ring is allocated only for a transport somebody reads. `PartnerHistory` pairs a frame with
|
|
229
|
+
the nearest partner within half the partner's frame period, measured from its samples, when no `max_offset_ns` is
|
|
230
|
+
given (the new default, also `RGBDInputs`' for the depth; a segmenter's instances still pair by equal stamps): a steady partner stream pairs every frame with its nearest sample,
|
|
231
|
+
whatever its fixed offset (the Azure Kinect's colour and depth_to_color: 11-17 ms), and a missing one is not
|
|
232
|
+
replaced by a neighbour; an explicit value overrides it, 0 asks for equal stamps. `PartnerHistory.wait(stamp,
|
|
233
|
+
timeout)` reads a frame's partner as it comes, for a node that takes a frame and then works on it;
|
|
234
|
+
`RGBDInputs.take(stamp)` reads all of a frame's streams together, and `FramePreparer` uses it before any work. A
|
|
235
|
+
partner overwritten in its ring before it was read counts in `PartnerHistory.lost` (`RGBDInputs.lost()`) and is
|
|
236
|
+
logged at most every 10 s, with the fix (take the pair together, or more slots).
|
|
237
|
+
* **0.3.43** A controller's commands addressed, whole and in order (the review of 2026-11-08): one
|
|
238
|
+
`moveit_msgs/msg/RobotState` per command on `<robot>/controller/<instance>/command` (joint targets and the controlled
|
|
239
|
+
frames' pose targets, one stamp), instead of `<robot>/command/tf` and `/command/joints` paired by stamp, which went to
|
|
240
|
+
every linked controller, split a command when a key came late and could apply an older command after a newer one. A
|
|
241
|
+
stale command (stamp not newer than the last one taken) is refused, so is one whose wall-clock stamp is more than
|
|
242
|
+
250 ms off the controller's clock (`CommandSubscriber(max_age=)`), a
|
|
243
|
+
command is taken or refused whole (a frame or joint the controller does not have or twice, mixed stamps), one
|
|
244
|
+
controller's commands are sent one at a time. New
|
|
245
|
+
`<instance>/status` every 20 ms: active, level, accepted and rejected commands, the link's own refusals,
|
|
246
|
+
`hardware_id` the controller's name. `durchblick.controller`: `CommandPublisher(session, robot, controller,
|
|
247
|
+
root_frame)`, `CommandSubscriber(..., on_refused=)`, `ControllerPublisher.status()`, `ControllerSubscriber(on_status=)`;
|
|
248
|
+
`msg.RobotState`, `msg.MultiDOFJointState`, `topics.controller_command`, `topics.STATUS`.
|
|
249
|
+
`<robot>/command/*` stays what a simulation records its robot was told.
|
|
250
|
+
* **0.3.42** A controller on Durchblick is named by its role, below `<robot>/controller/<role>`
|
|
251
|
+
(`robot/controller/arms`), as ros2_control names controllers. ArmarX integration: see durchblick_armarx.
|
|
252
|
+
* **0.3.41** ArmarX integration: see durchblick_armarx.
|
|
253
|
+
* **0.3.40** A robot's controllers on Durchblick (protocol.hpp, topics.py): what a controller is
|
|
254
|
+
told on `<robot>/command/tf` (pose targets) and `<robot>/command/joints` (joint targets; the two with the same stamp
|
|
255
|
+
belong together, ROS's exact-time pairing), what it reports below `<robot>/controller/<instance>`: `joint_states`,
|
|
256
|
+
`tf` (per arm `<tcp>/measured`, `/reference`, `/target`), `wrench/<arm>`, `record` (every value of every recorded
|
|
257
|
+
cycle), `diagnostics` and `hands/joint_states`, with the parameters `level`, `state_decimation` and
|
|
258
|
+
`record_fields`. Python `durchblick.controller`: both sides of it (`CommandPublisher`, `CommandSubscriber` with the
|
|
259
|
+
pairing, `ControllerPublisher`, `ControllerSubscriber`). New type
|
|
260
|
+
`std_msgs/Float64MultiArray` (Python and C++, with its MCAP definition), C++ `MessagePublisher` (one ROS type on one
|
|
261
|
+
key), `topics.controller()`; recordings take `<ns>/controller/**` by default. ArmarX integration: see durchblick_armarx.
|
|
262
|
+
* **0.3.39** Fix of 0.3.36's `local="auto"`: a subscriber without a device (`device=None`, the default; C++
|
|
263
|
+
`cuda_device` unset) works on the CPU and takes shared memory, `shm (this subscriber works on the CPU)`; a CUDA
|
|
264
|
+
ring only with a GPU device (`device="cuda:0"`, C++ `.cuda_device = 0`). 0.3.36 to 0.3.38 handed device arrays
|
|
265
|
+
to readers that never asked for a GPU (SAM 3's test reader).
|
|
266
|
+
* **0.3.38** Sessions close within 5 s: `open_session` and `open_from_args` return a `session.Session`, the
|
|
267
|
+
zenoh.Session with every attribute (and `isinstance(s, zenoh.Session)`) whose `close()` and the end of a `with`
|
|
268
|
+
block go through `session.close(…, timeout)`; a Zenoh close that does not return (after a query it did not
|
|
269
|
+
finalize) no longer blocks a program or the tests (the v0.3.37 pipeline hung in `with open_session(...)`).
|
|
270
|
+
* **0.3.37** Tests only: the viewer test skips without rerun-sdk (CI installs no extras), the not-found message is
|
|
271
|
+
its own test; test_point_clouds waits for the cloud placed in the colour camera's frame (a race on loaded hosts).
|
|
272
|
+
0.3.36's tag pipeline failed on the viewer test, so 0.3.37 is 0.3.36 released.
|
|
273
|
+
* **0.3.36** The play node's controls as parameters (node `durchblick/<name>`, recordings and videos): `paused`,
|
|
274
|
+
`position_s` (a seek: a video from the key frame before it decoded on to the frame, a recording from its log
|
|
275
|
+
time; the stamps keep growing; after the end it plays the files again), `loop` at run time, the read-only
|
|
276
|
+
`duration_s` and `file`, beside `rate`; ParameterEvents when the playback changes them itself
|
|
277
|
+
(`Parameters.refresh`, Python and C++); the terminal interface does not overwrite a field being edited.
|
|
278
|
+
`session.replies` reads a query's channel in a short thread instead of Python callbacks (no GIL handover per
|
|
279
|
+
reply; a CI runner under load had measured 12 ms). `durchblick rerun --spawn` finds the viewer next to Python
|
|
280
|
+
before PATH (a venv not activated). `blick --connect ...` (and `--listen`, `--zenoh-mode`, `--config`) starts
|
|
281
|
+
the terminal interface connected, a client by default with endpoints; as a peer it says when it cannot reach
|
|
282
|
+
the peers the admin space shows (a tunnel), and docs/tui.md has the SSH recipe. `durchblick rerun`: rates
|
|
283
|
+
(`rates/<topic>`, Hz), ages at receive (`ages_received/<topic>`), and `ages_now` as a Markdown table with the
|
|
284
|
+
samples a second lost on the way (seq gaps); views named with their units. mcap 1.6 or newer. Same-host frames
|
|
285
|
+
by `local="auto"` by default (Python, C++ `SubscriberOptions::local`): the publisher's CUDA ring if it is on the
|
|
286
|
+
subscriber's GPU (`device`/`cuda_device`, compared by UUID: `_core.cuda_device_uuid`), else shared memory; the
|
|
287
|
+
reason in `describe()`, `transport_reason` and the log; info records name the ring's GPU from the start;
|
|
288
|
+
`session.close(session, timeout)`: a Zenoh close that does not return (after a query Zenoh did not finalize)
|
|
289
|
+
no longer blocks the terminal interface or the tests; `session.replies` cancels a query at its deadline.
|
|
290
|
+
`node.add_arguments(parser, local=...)` adds `--local` (a CPU node: `("auto", "shm")`, `--local cuda` refused). `durchblick play --cuda-device` for videos: a CUDA ring
|
|
291
|
+
per stream.
|
|
292
|
+
* **0.3.35** Tests: a hard limit per test, pytest-timeout (a dev dependency) at 120 s, method thread: a hang prints
|
|
293
|
+
every thread's stack and ends the run instead of blocking CI until its job timeout; faulthandler's dump at 90 s
|
|
294
|
+
(was 300 s) before it ([development](docs/development.md)).
|
|
295
|
+
* **0.3.34** Queries never block beyond a deadline: `session.replies` (Python) and `durchblick::replies` (C++)
|
|
296
|
+
collect a query's replies until Zenoh ends it or its timeout and as much again (at least 0.5 s) have passed, then
|
|
297
|
+
go on with what came, count it and log it once per selector; every query of Durchblick goes through them (stream
|
|
298
|
+
info, `find_streams`, `tf_static`, the record and play node, the terminal interface and its survey, parameters,
|
|
299
|
+
liveliness). A get with timeout=1.0 had blocked for 5 min in CI ([sessions](docs/sessions.md)).
|
|
300
|
+
* **0.3.33** `durchblick play` plays videos and image sequences as camera streams ([video](docs/video.md); the extra
|
|
301
|
+
`durchblick[video]`, PyAV): any file that is not MCAP (by its content), several at once on one clock, one stream
|
|
302
|
+
each (`--stream`, `--camera` for one), stamps from the start's wall time and the frames' PTS (equal PTS: equal
|
|
303
|
+
stamps; `--offset-ms`), sent at their stamps, looped together; `--camera-info` or an assumed pinhole
|
|
304
|
+
(`--hfov`/`--vfov`), `--size`/`--scale`, `--start`/`--end`, `--fps`, `--frame-id`; 16-bit grey videos as
|
|
305
|
+
`16UC1`; source `video`; diagnostics `play` (frames/s, decode ms, position, late frames, send skew). The play
|
|
306
|
+
node's parameter `rate` changes a playback's rate while it plays (recordings too; `recording.Paced`, the pace
|
|
307
|
+
both share). `durchblick record` without the extra `record` fails at its start as before; `durchblick play`
|
|
308
|
+
of videos does not need it.
|
|
309
|
+
* **0.3.32** `Parameters.apply_all` (Python and C++) writes in the order the parameters were declared, not by
|
|
310
|
+
name: a setting before its `auto_*` bool, so a control given auto at the start stays auto (before, every such
|
|
311
|
+
control ended manual); a value a write changed elsewhere (the bool a setting switched to manual) is stored as it
|
|
312
|
+
is now, so `apply_all` writes what the device was left with. A partner subscriber (`keep_recent`, Python and C++)
|
|
313
|
+
no longer counts the samples it keeps as `overwritten`.
|
|
314
|
+
* **0.3.31** Node parameters as ROS 2's ([parameters](docs/parameters.md)): the rcl_interfaces messages
|
|
315
|
+
(`ParameterDescriptor`, `ParameterValue`, `ParameterEvent`, the services' requests and responses) in Python and C++,
|
|
316
|
+
ROS 2's six parameter services as queryables below the node, `ParameterEvent`s on `parameter_events`, the token
|
|
317
|
+
`<node>/parameters`; `node.Parameters` and C++ `durchblick::Parameters` (descriptors, checks, writers and readers,
|
|
318
|
+
`--param`, `--params-file`, `--dump-params`, `--params-read-only`), `ParameterClient`, `durchblick param`, the
|
|
319
|
+
terminal interface's Parameters tab; `durchblick-cpp-camera --params`, `durchblick-cpp-probe --params`. The terminal
|
|
320
|
+
interface takes the terminal's size also where `COLUMNS` and `LINES` are set (readline sets them for child processes
|
|
321
|
+
before Python 3.13; the interface stayed at 80 x 24 then).
|
|
322
|
+
* **0.3.30** The age at send of every sample with an attachment (`publish_ns` minus the stamp, on the publisher's
|
|
323
|
+
clock alone; `publish_ns` taken right before each transport's put, so with its encoding or ring copy, and
|
|
324
|
+
`publish_called_ns` when publish was called, Python and C++): `Frame.age_at_send_ns` (Python, C++ `Frame::age_at_send_ns()`, clouds too), `stamps.age_at_send_ns`
|
|
325
|
+
(`stamps.header_stamp`, which now also reads Durchblick's ring descriptors' stamps), detections publish the
|
|
326
|
+
attachment; shown by `durchblick hz`, the terminal interface (median / p95 at send and at receive, ms) and `durchblick
|
|
327
|
+
rerun` (`ages/<topic>`, `--no-ages`). `--max-hz` for nodes (`node.add_arguments`, `Reporter.watch`): subscriptions
|
|
328
|
+
with `max_hz` (Python and C++) take at most so many frames a second, the newest, chosen by their stamps so that a
|
|
329
|
+
camera's streams pair, and skip the others before decoding them; paired streams limited as a pair (only the
|
|
330
|
+
primary, the partners decoded at its frames' stamps: `history.PartnerHistory`, `RGBDInputs`' `max_offset_ns`, C++
|
|
331
|
+
`SubscriberOptions::keep_recent`, `ImageSubscriber::at`); a node's own default
|
|
332
|
+
(`node.add_arguments(parser, max_hz=30.0)`), `--max-hz 0` or `none` every frame it can take, the rate in effect in
|
|
333
|
+
the start log (`node.start(..., max_hz=)`) and in every report (`max-hz`); `durchblick-points --max-hz` (default
|
|
334
|
+
every frame), `durchblick-cpp-probe --max-hz`.
|
|
335
|
+
* **0.3.29** A terminal interface: `durchblick` without a command (Textual, a new dependency): connect and connect
|
|
336
|
+
again, topics with their traffic and last message, sessions, reports, the publish-subscribe graph from the routers'
|
|
337
|
+
admin space, and `durchblick rerun` from chosen streams and options with named presets (`survey.py`, `tui.py`).
|
|
338
|
+
`blick` is the same command as `durchblick`. Diagnostics samples (Python and C++ `DiagnosticsPublisher`) carry the
|
|
339
|
+
attachment with the publishing session's `zid` (`diagnostics.sender`); readers that ignore it read them as before.
|
|
340
|
+
`durchblick mps` announces itself on Zenoh (`durchblick/mps/<name>`, its status with GPUs and clients;
|
|
341
|
+
`--name`, `--connect`, `--listen`, `--config`) and the terminal interface shows it. The interface shows how each
|
|
342
|
+
consumer takes a stream (shm, cuda, or the Zenoh link's protocol with raw or compressed).
|
|
343
|
+
`--zenoh-mode peer|client` for every command (`session.add_arguments`); client mode refuses listen endpoints.
|
|
344
|
+
* **0.3.28** Recording simulations: blocking publishers (`blocking=True`, opt-in: the default drops; counts on
|
|
345
|
+
`<key>/sent`) and blocking recordings with a count check at stop (`RecordingError`); `record` with exactly the keys
|
|
346
|
+
given and a *ready* reply; log time from the messages' stamps (`--log-time header`); lockstep playback (`--pace
|
|
347
|
+
step` with the command `step`, `--pace clock`); commands as queries with their results (`recording.request`; status
|
|
348
|
+
version 3); a simulation's topics (`<world>/tf`, `clock`, `events`, `<robot>/command/joints`, `tf`;
|
|
349
|
+
`sim.SimPublisher`; `rosgraph_msgs/Clock`); `recording.Recording` in-process. A blocking recorder never blocks for,
|
|
350
|
+
nor checks, a dropping publisher's keys (reported as `dropping`). Python only: C++ publishers have no blocking mode.
|
|
351
|
+
Every `RobotStatePublisher` and `TransformPublisher` sample now carries the attachment.
|
|
352
|
+
* **0.3.27** `--loop` moves the stamps on by the recording's length each pass (they went back to the start, and nodes
|
|
353
|
+
that take only newer frames stopped after the first pass); the player's status counts the `passes`.
|
|
354
|
+
* **0.3.26** One node records and plays (`durchblick record`, `durchblick play`; Zenoh commands `record`, `stop`,
|
|
355
|
+
`play`, `pause`, `resume`, status version 2); played image streams go out as a camera's: decoded once into the
|
|
356
|
+
shared-memory ring (and a CUDA ring), the recorded JPEG or PNG unchanged (`CameraPublisher.publish_compressed`).
|
|
357
|
+
* **0.3.25** Python's `PointCloudPublisher` publishes an empty cloud (no points; it failed with "slot size 0");
|
|
358
|
+
`durchblick rerun` draws a node's cloud with pixels (`u, v`, durchblick-tapnext's tracks) over its image too.
|
|
359
|
+
* **0.3.24** `vision_msgs/Classification` (classes without a place, e.g. actions; Python and C++), published and
|
|
360
|
+
read like the detections, recorded, and shown by `durchblick rerun` as a text log.
|
|
361
|
+
* **0.3.23** Shared node code (`node`, `history`, `tensors`, `camera.check_local`, `poses.transform_of`/`pose_of`);
|
|
362
|
+
publishers' rings grow with a larger frame (Python and C++).
|
|
363
|
+
* **0.3.22** `uv sync` rebuilds the package when its C++ sources changed (it kept an old `durchblick._core` after a
|
|
364
|
+
pull).
|
|
365
|
+
* **0.3.21** The installed CMake package names the CUDA toolkit it was built with (its consumers find it).
|
|
366
|
+
* **0.3.20** `find_streams` skips an answer that is no info record instead of failing.
|
|
367
|
+
* **0.3.19** A frame whose ring is gone counts as stale instead of ending the reader; `FramePreparer` and the point
|
|
368
|
+
cloud node wait for a camera_info of the frames' size.
|
|
369
|
+
* **0.3.18** Logging for the nodes (`logs.setup`), `describe()` of a subscription, `RGBDInputs.waiting()`,
|
|
370
|
+
`DiagnosticsPublisher`.
|
|
371
|
+
* **0.3.17** `durchblick record`, `durchblick play` (MCAP).
|
|
372
|
+
* **0.3.16** `durchblick rerun`'s default layout; label images over their colour stream, id 0 transparent.
|
|
373
|
+
* **0.3.15** A Python `CameraPublisher` refuses a codec that cannot compress its encoding; `codec=None`.
|
|
374
|
+
* **0.3.14** `durchblick rerun` without options shows every node's results, also of nodes started later.
|
|
375
|
+
* **0.3.13** `durchblick rerun` draws a node's bodies (`<camera>/<node>/bodies`).
|
|
376
|
+
* **0.3.12** `rgbd.Distorter`: a node's per-pixel output back into the camera's own image.
|
|
377
|
+
* **0.3.11** `paths.expand_path`: `${VAR}` and `~` in a node's path arguments.
|
|
378
|
+
* **0.3.10** `rgbd.collect_views`: views of an object from a moving camera.
|
|
379
|
+
* **0.3.9** `durchblick.rgbd`: a camera's colour, depth and a segmenter's masks paired by stamp.
|
|
380
|
+
* **0.3.8** `durchblick rerun --segmentation` (label images).
|
|
381
|
+
* **0.3.7** `find_streams` asks again until its timeout (publishers behind a router).
|
|
382
|
+
* **0.3.6** Device arrays published complete and before `publish` returns (`stream=`); NVIDIA MPS (`durchblick mps`).
|
|
383
|
+
* **0.3.5** A Zenoh router on every PC, sessions without endpoints connect to it.
|
|
384
|
+
* **0.3.4** A robot's state (`<robot>/joint_states`, `tf`, `odom`, `wrench/<sensor>`), poses at an image's stamp
|
|
385
|
+
(`poses.py`). ArmarX integration: see durchblick_armarx.
|
|
386
|
+
* **0.3.3** A camera's pose on `<camera>/tf`; C++ `Pose` is ROS's `geometry_msgs/Pose` (0.3.2: `PoseMessage`).
|
|
387
|
+
* **0.3.2** Vision results as ROS's `vision_msgs` (`DetectionPublisher`, `DetectionSubscriber`).
|
|
388
|
+
* **0.3.1** `--max-depth` for the point cloud node; ROS calibration files for `CameraInfo`.
|
|
389
|
+
* **0.3.0** Point cloud streams and `durchblick-points`; `local="cuda"`; info record version 2 (0.2.x refused).
|
|
390
|
+
* **0.2.3** `<camera>/imu`, `<camera>/diagnostics`. **0.2.2** `<camera>/tf_static`. **0.2.1** The Rerun viewer, JPEG
|
|
391
|
+
pass-through for MJPEG cameras, device frames downloaded into the shared-memory ring.
|
xblick-0.5.0/CLAUDE.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|