| @@ -197,7 +197,7 @@ jobs: | |||||
| required-ros-distributions: humble | required-ros-distributions: humble | ||||
| - run: 'source /opt/ros/humble/setup.bash && echo AMENT_PREFIX_PATH=${AMENT_PREFIX_PATH} >> "$GITHUB_ENV"' | - run: 'source /opt/ros/humble/setup.bash && echo AMENT_PREFIX_PATH=${AMENT_PREFIX_PATH} >> "$GITHUB_ENV"' | ||||
| - name: Install the latest version of uv | - name: Install the latest version of uv | ||||
| uses: astral-sh/setup-uv@v5 | |||||
| uses: astral-sh/setup-uv@v6 | |||||
| with: | with: | ||||
| enable-cache: true | enable-cache: true | ||||
| - name: Install pyarrow | - name: Install pyarrow | ||||
| @@ -1,6 +1,9 @@ | |||||
| #!/bin/bash | #!/bin/bash | ||||
| set -euo | set -euo | ||||
| # Check if we are running in a GitHub Actions environment | |||||
| CI=${GITHUB_ACTIONS:-false} | |||||
| # List of ignored modules | # List of ignored modules | ||||
| ignored_folders=("dora-parler" "dora-opus" "dora-internvl" "dora-magma") | ignored_folders=("dora-parler" "dora-opus" "dora-internvl" "dora-magma") | ||||
| @@ -14,6 +17,30 @@ dir=$(pwd) | |||||
| base_dir=$(basename "$dir") | base_dir=$(basename "$dir") | ||||
| export GIT_LFS_SKIP_SMUDGE=1 | export GIT_LFS_SKIP_SMUDGE=1 | ||||
| # Large node list requiring space cleanup | |||||
| large_node=("dora-phi4") | |||||
| export PYTEST_ADDOPTS="-x" | |||||
| # Check if the current directory is in the large node list and if we're in the CI environment | |||||
| if [[ " ${large_node[@]} " =~ " ${base_dir} " ]] && [[ "$CI" == "true" ]]; then | |||||
| echo "Running cleanup for $base_dir..." | |||||
| sudo rm -rf /opt/hostedtoolcache/CodeQL || : | |||||
| # 1.4GB | |||||
| sudo rm -rf /opt/hostedtoolcache/go || : | |||||
| # 489MB | |||||
| sudo rm -rf /opt/hostedtoolcache/PyPy || : | |||||
| # 376MB | |||||
| sudo rm -rf /opt/hostedtoolcache/node || : | |||||
| # Remove Web browser packages | |||||
| sudo apt purge -y \ | |||||
| firefox \ | |||||
| google-chrome-stable \ | |||||
| microsoft-edge-stable | |||||
| sudo rm -rf /usr/local/lib/android/ | |||||
| sudo rm -rf /usr/share/dotnet/ | |||||
| sudo rm -rf /opt/ghc/ | |||||
| fi | |||||
| # Check if the directory name is in the ignored list | # Check if the directory name is in the ignored list | ||||
| if [[ " ${ignored_folders[@]} " =~ " ${base_dir} " ]]; then | if [[ " ${ignored_folders[@]} " =~ " ${base_dir} " ]]; then | ||||
| @@ -62,16 +89,8 @@ else | |||||
| if [ "$GITHUB_EVENT_NAME" == "release" ] || [ "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]; then | if [ "$GITHUB_EVENT_NAME" == "release" ] || [ "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]; then | ||||
| maturin publish --skip-existing | maturin publish --skip-existing | ||||
| fi | fi | ||||
| # x86_64-apple-darwin | |||||
| rustup target add x86_64-apple-darwin | |||||
| maturin build --target x86_64-apple-darwin --zig --release | |||||
| # If GITHUB_EVENT_NAME is release or workflow_dispatch, publish the wheel | |||||
| if [ "$GITHUB_EVENT_NAME" == "release" ] || [ "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]; then | |||||
| maturin publish --target x86_64-apple-darwin --skip-existing --zig | |||||
| fi | |||||
| elif [[ "$(uname)" = "Linux" ]]; then | |||||
| elif [[ "$(uname)" = "Linux" ]] || [[ "$CI" == "false" ]]; then | |||||
| if [ -f "$dir/Cargo.toml" ]; then | if [ -f "$dir/Cargo.toml" ]; then | ||||
| echo "Running build and tests for Rust project in $dir..." | echo "Running build and tests for Rust project in $dir..." | ||||
| cargo check | cargo check | ||||
| @@ -96,7 +115,7 @@ else | |||||
| else | else | ||||
| uv run pytest | uv run pytest | ||||
| fi | fi | ||||
| if [ "$GITHUB_EVENT_NAME" == "release" ] || [ "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]; then | |||||
| if [ "${GITHUB_EVENT_NAME:-false}" == "release" ] || [ "${GITHUB_EVENT_NAME:-false}" == "workflow_dispatch" ]; then | |||||
| uv build | uv build | ||||
| uv publish --check-url https://pypi.org/simple | uv publish --check-url https://pypi.org/simple | ||||
| fi | fi | ||||
| @@ -192,8 +192,6 @@ jobs: | |||||
| fail-fast: false | fail-fast: false | ||||
| matrix: | matrix: | ||||
| platform: | platform: | ||||
| - runner: macos-13 | |||||
| target: x86_64 | |||||
| - runner: macos-13 | - runner: macos-13 | ||||
| target: aarch64 | target: aarch64 | ||||
| repository: | repository: | ||||
| @@ -1165,9 +1165,9 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "avif-serialize" | name = "avif-serialize" | ||||
| version = "0.8.3" | |||||
| version = "0.8.5" | |||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "98922d6a4cfbcb08820c69d8eeccc05bb1f29bfa06b4f5b1dbfe9a868bd7608e" | |||||
| checksum = "2ea8ef51aced2b9191c08197f55450d830876d9933f8f48a429b354f1d496b42" | |||||
| dependencies = [ | dependencies = [ | ||||
| "arrayvec", | "arrayvec", | ||||
| ] | ] | ||||
| @@ -1303,7 +1303,7 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "benchmark-example-node" | name = "benchmark-example-node" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| dependencies = [ | dependencies = [ | ||||
| "dora-node-api", | "dora-node-api", | ||||
| "eyre", | "eyre", | ||||
| @@ -1316,7 +1316,7 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "benchmark-example-sink" | name = "benchmark-example-sink" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| dependencies = [ | dependencies = [ | ||||
| "dora-node-api", | "dora-node-api", | ||||
| "eyre", | "eyre", | ||||
| @@ -2089,7 +2089,7 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "communication-layer-pub-sub" | name = "communication-layer-pub-sub" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| dependencies = [ | dependencies = [ | ||||
| "flume 0.10.14", | "flume 0.10.14", | ||||
| "zenoh 0.7.0-rc", | "zenoh 0.7.0-rc", | ||||
| @@ -2097,7 +2097,7 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "communication-layer-request-reply" | name = "communication-layer-request-reply" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| [[package]] | [[package]] | ||||
| name = "concat-idents" | name = "concat-idents" | ||||
| @@ -2279,6 +2279,21 @@ dependencies = [ | |||||
| "libc", | "libc", | ||||
| ] | ] | ||||
| [[package]] | |||||
| name = "crc" | |||||
| version = "3.3.0" | |||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | |||||
| checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" | |||||
| dependencies = [ | |||||
| "crc-catalog", | |||||
| ] | |||||
| [[package]] | |||||
| name = "crc-catalog" | |||||
| version = "2.4.0" | |||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | |||||
| checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" | |||||
| [[package]] | [[package]] | ||||
| name = "crc32fast" | name = "crc32fast" | ||||
| version = "1.4.2" | version = "1.4.2" | ||||
| @@ -2911,7 +2926,7 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "dora-arrow-convert" | name = "dora-arrow-convert" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| dependencies = [ | dependencies = [ | ||||
| "arrow", | "arrow", | ||||
| "chrono", | "chrono", | ||||
| @@ -2922,7 +2937,7 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "dora-cli" | name = "dora-cli" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| dependencies = [ | dependencies = [ | ||||
| "bat", | "bat", | ||||
| "clap 4.5.32", | "clap 4.5.32", | ||||
| @@ -2966,7 +2981,7 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "dora-coordinator" | name = "dora-coordinator" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| dependencies = [ | dependencies = [ | ||||
| "ctrlc", | "ctrlc", | ||||
| "dora-core", | "dora-core", | ||||
| @@ -2987,7 +3002,7 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "dora-core" | name = "dora-core" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| dependencies = [ | dependencies = [ | ||||
| "dora-message", | "dora-message", | ||||
| "dunce", | "dunce", | ||||
| @@ -3011,7 +3026,7 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "dora-daemon" | name = "dora-daemon" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| dependencies = [ | dependencies = [ | ||||
| "aligned-vec", | "aligned-vec", | ||||
| "async-trait", | "async-trait", | ||||
| @@ -3048,7 +3063,7 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "dora-dav1d" | name = "dora-dav1d" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| dependencies = [ | dependencies = [ | ||||
| "bitstream-io", | "bitstream-io", | ||||
| "bytemuck", | "bytemuck", | ||||
| @@ -3062,7 +3077,7 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "dora-download" | name = "dora-download" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| dependencies = [ | dependencies = [ | ||||
| "eyre", | "eyre", | ||||
| "reqwest", | "reqwest", | ||||
| @@ -3092,7 +3107,7 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "dora-kit-car" | name = "dora-kit-car" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| dependencies = [ | dependencies = [ | ||||
| "dora-node-api", | "dora-node-api", | ||||
| "dotenv", | "dotenv", | ||||
| @@ -3106,7 +3121,7 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "dora-message" | name = "dora-message" | ||||
| version = "0.5.0-alpha" | |||||
| version = "0.5.0" | |||||
| dependencies = [ | dependencies = [ | ||||
| "aligned-vec", | "aligned-vec", | ||||
| "arrow-data", | "arrow-data", | ||||
| @@ -3127,7 +3142,7 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "dora-metrics" | name = "dora-metrics" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| dependencies = [ | dependencies = [ | ||||
| "eyre", | "eyre", | ||||
| "opentelemetry 0.29.1", | "opentelemetry 0.29.1", | ||||
| @@ -3148,7 +3163,7 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "dora-node-api" | name = "dora-node-api" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| dependencies = [ | dependencies = [ | ||||
| "aligned-vec", | "aligned-vec", | ||||
| "arrow", | "arrow", | ||||
| @@ -3173,7 +3188,7 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "dora-node-api-c" | name = "dora-node-api-c" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| dependencies = [ | dependencies = [ | ||||
| "arrow-array", | "arrow-array", | ||||
| "dora-node-api", | "dora-node-api", | ||||
| @@ -3183,7 +3198,7 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "dora-node-api-cxx" | name = "dora-node-api-cxx" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| dependencies = [ | dependencies = [ | ||||
| "arrow", | "arrow", | ||||
| "cxx", | "cxx", | ||||
| @@ -3201,7 +3216,7 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "dora-node-api-python" | name = "dora-node-api-python" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| dependencies = [ | dependencies = [ | ||||
| "arrow", | "arrow", | ||||
| "dora-cli", | "dora-cli", | ||||
| @@ -3222,7 +3237,7 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "dora-object-to-pose" | name = "dora-object-to-pose" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| dependencies = [ | dependencies = [ | ||||
| "dora-node-api", | "dora-node-api", | ||||
| "eyre", | "eyre", | ||||
| @@ -3231,7 +3246,7 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "dora-openai-proxy-server" | name = "dora-openai-proxy-server" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| dependencies = [ | dependencies = [ | ||||
| "chrono", | "chrono", | ||||
| "dora-node-api", | "dora-node-api", | ||||
| @@ -3252,7 +3267,7 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "dora-operator-api" | name = "dora-operator-api" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| dependencies = [ | dependencies = [ | ||||
| "dora-arrow-convert", | "dora-arrow-convert", | ||||
| "dora-operator-api-macros", | "dora-operator-api-macros", | ||||
| @@ -3261,14 +3276,14 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "dora-operator-api-c" | name = "dora-operator-api-c" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| dependencies = [ | dependencies = [ | ||||
| "dora-operator-api-types", | "dora-operator-api-types", | ||||
| ] | ] | ||||
| [[package]] | [[package]] | ||||
| name = "dora-operator-api-cxx" | name = "dora-operator-api-cxx" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| dependencies = [ | dependencies = [ | ||||
| "cxx", | "cxx", | ||||
| "cxx-build", | "cxx-build", | ||||
| @@ -3277,7 +3292,7 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "dora-operator-api-macros" | name = "dora-operator-api-macros" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| dependencies = [ | dependencies = [ | ||||
| "proc-macro2", | "proc-macro2", | ||||
| "quote", | "quote", | ||||
| @@ -3286,7 +3301,7 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "dora-operator-api-python" | name = "dora-operator-api-python" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| dependencies = [ | dependencies = [ | ||||
| "aligned-vec", | "aligned-vec", | ||||
| "arrow", | "arrow", | ||||
| @@ -3302,7 +3317,7 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "dora-operator-api-types" | name = "dora-operator-api-types" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| dependencies = [ | dependencies = [ | ||||
| "arrow", | "arrow", | ||||
| "dora-arrow-convert", | "dora-arrow-convert", | ||||
| @@ -3311,12 +3326,13 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "dora-rav1e" | name = "dora-rav1e" | ||||
| version = "0.3.11+fix1" | |||||
| version = "0.3.12" | |||||
| dependencies = [ | dependencies = [ | ||||
| "avif-serialize", | "avif-serialize", | ||||
| "bytemuck", | "bytemuck", | ||||
| "dora-node-api", | "dora-node-api", | ||||
| "eyre", | "eyre", | ||||
| "little_exif", | |||||
| "log", | "log", | ||||
| "pyo3", | "pyo3", | ||||
| "rav1e", | "rav1e", | ||||
| @@ -3324,7 +3340,7 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "dora-record" | name = "dora-record" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| dependencies = [ | dependencies = [ | ||||
| "chrono", | "chrono", | ||||
| "dora-node-api", | "dora-node-api", | ||||
| @@ -3336,7 +3352,7 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "dora-rerun" | name = "dora-rerun" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| dependencies = [ | dependencies = [ | ||||
| "bytemuck", | "bytemuck", | ||||
| "dora-node-api", | "dora-node-api", | ||||
| @@ -3351,7 +3367,7 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "dora-ros2-bridge" | name = "dora-ros2-bridge" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| dependencies = [ | dependencies = [ | ||||
| "array-init", | "array-init", | ||||
| "dora-daemon", | "dora-daemon", | ||||
| @@ -3374,7 +3390,7 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "dora-ros2-bridge-msg-gen" | name = "dora-ros2-bridge-msg-gen" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| dependencies = [ | dependencies = [ | ||||
| "anyhow", | "anyhow", | ||||
| "glob", | "glob", | ||||
| @@ -3390,7 +3406,7 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "dora-ros2-bridge-python" | name = "dora-ros2-bridge-python" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| dependencies = [ | dependencies = [ | ||||
| "arrow", | "arrow", | ||||
| "dora-ros2-bridge", | "dora-ros2-bridge", | ||||
| @@ -3404,7 +3420,7 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "dora-runtime" | name = "dora-runtime" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| dependencies = [ | dependencies = [ | ||||
| "aligned-vec", | "aligned-vec", | ||||
| "arrow", | "arrow", | ||||
| @@ -3443,7 +3459,7 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "dora-tracing" | name = "dora-tracing" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| dependencies = [ | dependencies = [ | ||||
| "eyre", | "eyre", | ||||
| "opentelemetry 0.18.0", | "opentelemetry 0.18.0", | ||||
| @@ -6378,6 +6394,19 @@ version = "0.4.1" | |||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" | checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" | ||||
| [[package]] | |||||
| name = "little_exif" | |||||
| version = "0.6.9" | |||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | |||||
| checksum = "9ebaa2b90127fb33dcf2ec1f5be8b496e92d5d1736789decca8eb2798c52f2e7" | |||||
| dependencies = [ | |||||
| "crc", | |||||
| "log", | |||||
| "miniz_oxide", | |||||
| "paste", | |||||
| "quick-xml 0.37.5", | |||||
| ] | |||||
| [[package]] | [[package]] | ||||
| name = "llguidance" | name = "llguidance" | ||||
| version = "0.7.0" | version = "0.7.0" | ||||
| @@ -6738,9 +6767,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" | |||||
| [[package]] | [[package]] | ||||
| name = "miniz_oxide" | name = "miniz_oxide" | ||||
| version = "0.8.5" | |||||
| version = "0.8.9" | |||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" | |||||
| checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" | |||||
| dependencies = [ | dependencies = [ | ||||
| "adler2", | "adler2", | ||||
| "simd-adler32", | "simd-adler32", | ||||
| @@ -6982,7 +7011,7 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "multiple-daemons-example-node" | name = "multiple-daemons-example-node" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| dependencies = [ | dependencies = [ | ||||
| "dora-node-api", | "dora-node-api", | ||||
| "eyre", | "eyre", | ||||
| @@ -6993,14 +7022,14 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "multiple-daemons-example-operator" | name = "multiple-daemons-example-operator" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| dependencies = [ | dependencies = [ | ||||
| "dora-operator-api", | "dora-operator-api", | ||||
| ] | ] | ||||
| [[package]] | [[package]] | ||||
| name = "multiple-daemons-example-sink" | name = "multiple-daemons-example-sink" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| dependencies = [ | dependencies = [ | ||||
| "dora-node-api", | "dora-node-api", | ||||
| "eyre", | "eyre", | ||||
| @@ -9028,9 +9057,9 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "quick-xml" | name = "quick-xml" | ||||
| version = "0.37.2" | |||||
| version = "0.37.5" | |||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003" | |||||
| checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" | |||||
| dependencies = [ | dependencies = [ | ||||
| "memchr", | "memchr", | ||||
| ] | ] | ||||
| @@ -11017,7 +11046,7 @@ checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430" | |||||
| [[package]] | [[package]] | ||||
| name = "receive_data" | name = "receive_data" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| dependencies = [ | dependencies = [ | ||||
| "chrono", | "chrono", | ||||
| "dora-node-api", | "dora-node-api", | ||||
| @@ -11457,7 +11486,7 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "rust-dataflow-example-node" | name = "rust-dataflow-example-node" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| dependencies = [ | dependencies = [ | ||||
| "dora-node-api", | "dora-node-api", | ||||
| "eyre", | "eyre", | ||||
| @@ -11468,7 +11497,7 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "rust-dataflow-example-sink" | name = "rust-dataflow-example-sink" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| dependencies = [ | dependencies = [ | ||||
| "dora-node-api", | "dora-node-api", | ||||
| "eyre", | "eyre", | ||||
| @@ -11476,7 +11505,7 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "rust-dataflow-example-sink-dynamic" | name = "rust-dataflow-example-sink-dynamic" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| dependencies = [ | dependencies = [ | ||||
| "dora-node-api", | "dora-node-api", | ||||
| "eyre", | "eyre", | ||||
| @@ -11484,7 +11513,7 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "rust-dataflow-example-status-node" | name = "rust-dataflow-example-status-node" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| dependencies = [ | dependencies = [ | ||||
| "dora-node-api", | "dora-node-api", | ||||
| "eyre", | "eyre", | ||||
| @@ -11503,7 +11532,7 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "rust-ros2-dataflow-example-node" | name = "rust-ros2-dataflow-example-node" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| dependencies = [ | dependencies = [ | ||||
| "dora-node-api", | "dora-node-api", | ||||
| "dora-ros2-bridge", | "dora-ros2-bridge", | ||||
| @@ -11999,7 +12028,7 @@ dependencies = [ | |||||
| "hyper 1.6.0", | "hyper 1.6.0", | ||||
| "indicatif", | "indicatif", | ||||
| "log", | "log", | ||||
| "quick-xml 0.37.2", | |||||
| "quick-xml 0.37.5", | |||||
| "regex", | "regex", | ||||
| "reqwest", | "reqwest", | ||||
| "self-replace", | "self-replace", | ||||
| @@ -12347,7 +12376,7 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "shared-memory-server" | name = "shared-memory-server" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| dependencies = [ | dependencies = [ | ||||
| "bincode", | "bincode", | ||||
| "eyre", | "eyre", | ||||
| @@ -13173,7 +13202,7 @@ dependencies = [ | |||||
| [[package]] | [[package]] | ||||
| name = "terminal-print" | name = "terminal-print" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| dependencies = [ | dependencies = [ | ||||
| "dora-node-api", | "dora-node-api", | ||||
| "eyre", | "eyre", | ||||
| @@ -14604,7 +14633,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||||
| checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484" | checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484" | ||||
| dependencies = [ | dependencies = [ | ||||
| "proc-macro2", | "proc-macro2", | ||||
| "quick-xml 0.37.2", | |||||
| "quick-xml 0.37.5", | |||||
| "quote", | "quote", | ||||
| ] | ] | ||||
| @@ -50,37 +50,37 @@ members = [ | |||||
| [workspace.package] | [workspace.package] | ||||
| edition = "2021" | edition = "2021" | ||||
| # Make sure to also bump `apis/node/python/__init__.py` version. | # Make sure to also bump `apis/node/python/__init__.py` version. | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| description = "`dora` goal is to be a low latency, composable, and distributed data flow." | description = "`dora` goal is to be a low latency, composable, and distributed data flow." | ||||
| documentation = "https://dora-rs.ai" | documentation = "https://dora-rs.ai" | ||||
| license = "Apache-2.0" | license = "Apache-2.0" | ||||
| repository = "https://github.com/dora-rs/dora/" | repository = "https://github.com/dora-rs/dora/" | ||||
| [workspace.dependencies] | [workspace.dependencies] | ||||
| dora-node-api = { version = "0.3.11", path = "apis/rust/node", default-features = false } | |||||
| dora-node-api-python = { version = "0.3.11", path = "apis/python/node", default-features = false } | |||||
| dora-operator-api = { version = "0.3.11", path = "apis/rust/operator", default-features = false } | |||||
| dora-operator-api-macros = { version = "0.3.11", path = "apis/rust/operator/macros" } | |||||
| dora-operator-api-types = { version = "0.3.11", path = "apis/rust/operator/types" } | |||||
| dora-operator-api-python = { version = "0.3.11", path = "apis/python/operator" } | |||||
| dora-operator-api-c = { version = "0.3.11", path = "apis/c/operator" } | |||||
| dora-node-api-c = { version = "0.3.11", path = "apis/c/node" } | |||||
| dora-core = { version = "0.3.11", path = "libraries/core" } | |||||
| dora-arrow-convert = { version = "0.3.11", path = "libraries/arrow-convert" } | |||||
| dora-tracing = { version = "0.3.11", path = "libraries/extensions/telemetry/tracing" } | |||||
| dora-metrics = { version = "0.3.11", path = "libraries/extensions/telemetry/metrics" } | |||||
| dora-download = { version = "0.3.11", path = "libraries/extensions/download" } | |||||
| shared-memory-server = { version = "0.3.11", path = "libraries/shared-memory-server" } | |||||
| communication-layer-request-reply = { version = "0.3.11", path = "libraries/communication-layer/request-reply" } | |||||
| dora-cli = { version = "0.3.11", path = "binaries/cli" } | |||||
| dora-runtime = { version = "0.3.11", path = "binaries/runtime" } | |||||
| dora-daemon = { version = "0.3.11", path = "binaries/daemon" } | |||||
| dora-coordinator = { version = "0.3.11", path = "binaries/coordinator" } | |||||
| dora-ros2-bridge = { version = "0.3.11", path = "libraries/extensions/ros2-bridge" } | |||||
| dora-ros2-bridge-msg-gen = { version = "0.3.11", path = "libraries/extensions/ros2-bridge/msg-gen" } | |||||
| dora-node-api = { version = "0.3.12", path = "apis/rust/node", default-features = false } | |||||
| dora-node-api-python = { version = "0.3.12", path = "apis/python/node", default-features = false } | |||||
| dora-operator-api = { version = "0.3.12", path = "apis/rust/operator", default-features = false } | |||||
| dora-operator-api-macros = { version = "0.3.12", path = "apis/rust/operator/macros" } | |||||
| dora-operator-api-types = { version = "0.3.12", path = "apis/rust/operator/types" } | |||||
| dora-operator-api-python = { version = "0.3.12", path = "apis/python/operator" } | |||||
| dora-operator-api-c = { version = "0.3.12", path = "apis/c/operator" } | |||||
| dora-node-api-c = { version = "0.3.12", path = "apis/c/node" } | |||||
| dora-core = { version = "0.3.12", path = "libraries/core" } | |||||
| dora-arrow-convert = { version = "0.3.12", path = "libraries/arrow-convert" } | |||||
| dora-tracing = { version = "0.3.12", path = "libraries/extensions/telemetry/tracing" } | |||||
| dora-metrics = { version = "0.3.12", path = "libraries/extensions/telemetry/metrics" } | |||||
| dora-download = { version = "0.3.12", path = "libraries/extensions/download" } | |||||
| shared-memory-server = { version = "0.3.12", path = "libraries/shared-memory-server" } | |||||
| communication-layer-request-reply = { version = "0.3.12", path = "libraries/communication-layer/request-reply" } | |||||
| dora-cli = { version = "0.3.12", path = "binaries/cli" } | |||||
| dora-runtime = { version = "0.3.12", path = "binaries/runtime" } | |||||
| dora-daemon = { version = "0.3.12", path = "binaries/daemon" } | |||||
| dora-coordinator = { version = "0.3.12", path = "binaries/coordinator" } | |||||
| dora-ros2-bridge = { version = "0.3.12", path = "libraries/extensions/ros2-bridge" } | |||||
| dora-ros2-bridge-msg-gen = { version = "0.3.12", path = "libraries/extensions/ros2-bridge/msg-gen" } | |||||
| dora-ros2-bridge-python = { path = "libraries/extensions/ros2-bridge/python" } | dora-ros2-bridge-python = { path = "libraries/extensions/ros2-bridge/python" } | ||||
| # versioned independently from the other dora crates | # versioned independently from the other dora crates | ||||
| dora-message = { version = "0.5.0-alpha", path = "libraries/message" } | |||||
| dora-message = { version = "0.5.0", path = "libraries/message" } | |||||
| arrow = { version = "54.2.1" } | arrow = { version = "54.2.1" } | ||||
| arrow-schema = { version = "54.2.1" } | arrow-schema = { version = "54.2.1" } | ||||
| arrow-data = { version = "54.2.1" } | arrow-data = { version = "54.2.1" } | ||||
| @@ -1,5 +1,59 @@ | |||||
| # Changelog | # Changelog | ||||
| ## v0.3.12 (2025-06-30) | |||||
| ## What's Changed | |||||
| - Implemented dora-cotracker node by @ShashwatPatil in https://github.com/dora-rs/dora/pull/931 | |||||
| - Minor fix and add boxes2d example to facebook/cotracker by @haixuanTao in https://github.com/dora-rs/dora/pull/950 | |||||
| - Update Rust crate tokio to v1.44.2 [SECURITY] by @renovate in https://github.com/dora-rs/dora/pull/951 | |||||
| - Post 3.11 release fix by @haixuanTao in https://github.com/dora-rs/dora/pull/954 | |||||
| - Bump crossbeam-channel from 0.5.14 to 0.5.15 by @dependabot in https://github.com/dora-rs/dora/pull/959 | |||||
| - Added E ruff flag for pydocstyle by @7SOMAY in https://github.com/dora-rs/dora/pull/958 | |||||
| - Revert "Added E ruff flag for better code quality [skip ci]" by @haixuanTao in https://github.com/dora-rs/dora/pull/968 | |||||
| - Ease of use changes in benches for issue #957 by @Ignavar in https://github.com/dora-rs/dora/pull/969 | |||||
| - Reachy cotracker by @haixuanTao in https://github.com/dora-rs/dora/pull/972 | |||||
| - Improve rav1e by @haixuanTao in https://github.com/dora-rs/dora/pull/974 | |||||
| - Fix pyrealsense by @haixuanTao in https://github.com/dora-rs/dora/pull/973 | |||||
| - Added Self Uninstall Command by @Shar-jeel-Sajid in https://github.com/dora-rs/dora/pull/944 | |||||
| - Improve benchmark implementation & Add warning for discarding events by @Mivik in https://github.com/dora-rs/dora/pull/971 | |||||
| - docs: Updated README: Added comprehensive usage documentation with vi… by @LeonRust in https://github.com/dora-rs/dora/pull/983 | |||||
| - Fix rerun-viewer example. by @francocipollone in https://github.com/dora-rs/dora/pull/989 | |||||
| - docs: add license badge by @Radovenchyk in https://github.com/dora-rs/dora/pull/996 | |||||
| - Disable sccache for `musllinux` jobs by @haixuanTao in https://github.com/dora-rs/dora/pull/1000 | |||||
| - Remove unused sysinfo monitor by @Mivik in https://github.com/dora-rs/dora/pull/1007 | |||||
| - Refactor Python CUDA IPC API by @Mivik in https://github.com/dora-rs/dora/pull/1002 | |||||
| - fix terminal not printing stdout on nvml warning by @haixuanTao in https://github.com/dora-rs/dora/pull/1008 | |||||
| - Fix issue #1006: [Brief description of the fix] by @sohamukute in https://github.com/dora-rs/dora/pull/1013 | |||||
| - Improving so100 usability by @haixuanTao in https://github.com/dora-rs/dora/pull/988 | |||||
| - Add dora-mediapipe node for quick human pose estimation by @haixuanTao in https://github.com/dora-rs/dora/pull/986 | |||||
| - Bump torch to 2.7 by @haixuanTao in https://github.com/dora-rs/dora/pull/1015 | |||||
| - refactor(tracing): use builder style by @sjfhsjfh in https://github.com/dora-rs/dora/pull/1009 | |||||
| - Fix spawning runtime through python when it is installed with pip by @haixuanTao in https://github.com/dora-rs/dora/pull/1011 | |||||
| - chore(deps): update dependency numpy to v2 by @renovate in https://github.com/dora-rs/dora/pull/1014 | |||||
| - Fix error when multiple visualization key is active and when urdf_transform env variable is not present by @haixuanTao in https://github.com/dora-rs/dora/pull/1016 | |||||
| - Update pyrealsense2 Dependencies for L515 Support and Fix README wget Link by @kingchou007 in https://github.com/dora-rs/dora/pull/1021 | |||||
| - Minor fix for mujoco sim by @haixuanTao in https://github.com/dora-rs/dora/pull/1023 | |||||
| - dora-mujoco simulation node with example for controlling any arm by @ShashwatPatil in https://github.com/dora-rs/dora/pull/1012 | |||||
| - fix ros CI/CD by @haixuanTao in https://github.com/dora-rs/dora/pull/1027 | |||||
| - dora-vggt by @haixuanTao in https://github.com/dora-rs/dora/pull/1024 | |||||
| - Adding vision to openai server by @haixuanTao in https://github.com/dora-rs/dora/pull/1025 | |||||
| - Revert "Adding vision to openai server" by @haixuanTao in https://github.com/dora-rs/dora/pull/1031 | |||||
| - Expose AllInputClosed message as a Stop message by @haixuanTao in https://github.com/dora-rs/dora/pull/1026 | |||||
| - Add support for git repository sources for nodes by @phil-opp in https://github.com/dora-rs/dora/pull/901 | |||||
| - Adding vision to rust openai proxy server by @haixuanTao in https://github.com/dora-rs/dora/pull/1033 | |||||
| - Add automatic robot descriptions URDF retrieval from https://github.com/robot-descriptions/robot_descriptions.py by @haixuanTao in https://github.com/dora-rs/dora/pull/1032 | |||||
| ## New Contributors | |||||
| - @Mivik made their first contribution in https://github.com/dora-rs/dora/pull/971 | |||||
| - @francocipollone made their first contribution in https://github.com/dora-rs/dora/pull/989 | |||||
| - @sohamukute made their first contribution in https://github.com/dora-rs/dora/pull/1013 | |||||
| - @sjfhsjfh made their first contribution in https://github.com/dora-rs/dora/pull/1009 | |||||
| - @kingchou007 made their first contribution in https://github.com/dora-rs/dora/pull/1021 | |||||
| **Full Changelog**: https://github.com/dora-rs/dora/compare/v0.3.11...v0.3.12 | |||||
| ## v0.3.11 (2025-04-07) | ## v0.3.11 (2025-04-07) | ||||
| ## What's Changed | ## What's Changed | ||||
| @@ -62,6 +62,8 @@ | |||||
| <details open> | <details open> | ||||
| <summary><b>2025</b></summary> | <summary><b>2025</b></summary> | ||||
| - \[07/25\] Added Kornia rust nodes in the hub for V4L / Gstreamer cameras and Sobel image processing. | |||||
| - \[06/25\] Add support for git based node, dora-vggt for multi-camera depth estimation, and adding robot_descriptions_py as a default way to get urdfs within dora. | |||||
| - \[05/25\] Add support for dora-pytorch-kinematics for fk and ik, dora-mediapipe for pose estimation, dora-rustypot for rust serialport read/write, points2d and points3d visualization in rerun. | - \[05/25\] Add support for dora-pytorch-kinematics for fk and ik, dora-mediapipe for pose estimation, dora-rustypot for rust serialport read/write, points2d and points3d visualization in rerun. | ||||
| - \[04/25\] Add support for dora-cotracker to track any point on a frame, dora-rav1e AV1 encoding up to 12bit and dora-dav1d AV1 decoding, | - \[04/25\] Add support for dora-cotracker to track any point on a frame, dora-rav1e AV1 encoding up to 12bit and dora-dav1d AV1 decoding, | ||||
| - \[03/25\] Add support for dora async Python. | - \[03/25\] Add support for dora async Python. | ||||
| @@ -74,16 +76,16 @@ | |||||
| ## Support Matrix | ## Support Matrix | ||||
| | | dora-rs | | |||||
| | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | |||||
| | **APIs** | Python >= 3.7 including sync ⭐✅ <br> Rust ✅<br> C/C++ 🆗 <br>ROS2 >= Foxy 🆗 | | |||||
| | **OS** | Linux: Arm 32 ⭐✅ Arm 64 ⭐✅ x64_86 ⭐✅ <br>MacOS: Arm 64 ⭐✅ x64_86 ✅<br>Windows: x64_86 🆗 <br>WSL: x64_86 🆗 <br> Android: 🛠️ (Blocked by: https://github.com/elast0ny/shared_memory/issues/32) <br> IOS: 🛠️ | | |||||
| | **Message Format** | Arrow ✅ <br> Standard Specification 🛠️ | | |||||
| | **Local Communication** | Shared Memory ✅ <br> [Cuda IPC](https://arrow.apache.org/docs/python/api/cuda.html) 📐 | | |||||
| | **Remote Communication** | [Zenoh](https://zenoh.io/) 📐 | | |||||
| | **Metrics, Tracing, and Logging** | Opentelemetry 📐 | | |||||
| | **Configuration** | YAML ✅ | | |||||
| | **Package Manager** | [pip](https://pypi.org/): Python Node ✅ Rust Node ✅ C/C++ Node 🛠️ <br>[cargo](https://crates.io/): Rust Node ✅ | | |||||
| | | dora-rs | | |||||
| | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | |||||
| | **APIs** | Python >= 3.7 including sync ⭐✅ <br> Rust ✅<br> C/C++ 🆗 <br>ROS2 >= Foxy 🆗 | | |||||
| | **OS** | Linux: Arm 32 ⭐✅ Arm 64 ⭐✅ x64_86 ⭐✅ <br>MacOS: Arm 64 ⭐✅ <br>Windows: x64_86 🆗 <br>WSL: x64_86 🆗 <br> Android: 🛠️ (Blocked by: https://github.com/elast0ny/shared_memory/issues/32) <br> IOS: 🛠️ | | |||||
| | **Message Format** | Arrow ✅ <br> Standard Specification 🛠️ | | |||||
| | **Local Communication** | Shared Memory ✅ <br> [Cuda IPC](https://arrow.apache.org/docs/python/api/cuda.html) 📐 | | |||||
| | **Remote Communication** | [Zenoh](https://zenoh.io/) 📐 | | |||||
| | **Metrics, Tracing, and Logging** | Opentelemetry 📐 | | |||||
| | **Configuration** | YAML ✅ | | |||||
| | **Package Manager** | [pip](https://pypi.org/): Python Node ✅ Rust Node ✅ C/C++ Node 🛠️ <br>[cargo](https://crates.io/): Rust Node ✅ | | |||||
| > - ⭐ = Recommended | > - ⭐ = Recommended | ||||
| > - ✅ = First Class Support | > - ✅ = First Class Support | ||||
| @@ -102,6 +104,8 @@ | |||||
| | Camera | [PyOrbbeckSDK](https://github.com/dora-rs/dora/blob/main/node-hub/dora-pyorbbecksdk) | 📐 | Image and depth from Orbbeck Camera |  |  | | | Camera | [PyOrbbeckSDK](https://github.com/dora-rs/dora/blob/main/node-hub/dora-pyorbbecksdk) | 📐 | Image and depth from Orbbeck Camera |  |  | | ||||
| | Camera | [PyRealsense](https://github.com/dora-rs/dora/blob/main/node-hub/dora-pyrealsense) | Linux🆗 <br> Mac🛠️ | Image and depth from Realsense |  |  | | | Camera | [PyRealsense](https://github.com/dora-rs/dora/blob/main/node-hub/dora-pyrealsense) | Linux🆗 <br> Mac🛠️ | Image and depth from Realsense |  |  | | ||||
| | Camera | [OpenCV Video Capture](https://github.com/dora-rs/dora/blob/main/node-hub/opencv-video-capture) | ✅ | Image stream from OpenCV Camera |  |  | | | Camera | [OpenCV Video Capture](https://github.com/dora-rs/dora/blob/main/node-hub/opencv-video-capture) | ✅ | Image stream from OpenCV Camera |  |  | | ||||
| | Camera | [Kornia V4L Capture](https://github.com/kornia/dora-nodes-hub/tree/main/kornia-v4l-capture) | ✅ | Video stream for Linux Camera (rust) | |  | | |||||
| | Camera | [Kornia GST Capture](https://github.com/kornia/dora-nodes-hub/tree/main/kornia-gst-capture) | ✅ | Video Capture using Gstreamer (rust) | |  | | |||||
| | Peripheral | [Keyboard](https://github.com/dora-rs/dora/blob/main/node-hub/dora-keyboard) | ✅ | Keyboard char listener |  |  | | | Peripheral | [Keyboard](https://github.com/dora-rs/dora/blob/main/node-hub/dora-keyboard) | ✅ | Keyboard char listener |  |  | | ||||
| | Peripheral | [Microphone](https://github.com/dora-rs/dora/blob/main/node-hub/dora-microphone) | ✅ | Audio from microphone |  |  | | | Peripheral | [Microphone](https://github.com/dora-rs/dora/blob/main/node-hub/dora-microphone) | ✅ | Audio from microphone |  |  | | ||||
| | Peripheral | [PyAudio(Speaker)](https://github.com/dora-rs/dora/blob/main/node-hub/dora-pyaudio) | ✅ | Output audio from speaker |  |  | | | Peripheral | [PyAudio(Speaker)](https://github.com/dora-rs/dora/blob/main/node-hub/dora-pyaudio) | ✅ | Output audio from speaker |  |  | | ||||
| @@ -134,6 +138,7 @@ | |||||
| | Simulator | [Mujoco](https://github.com/dora-rs/dora-lerobot/blob/main/node-hub/mujoco-client) | 📐 | Mujoco Simulator | | | | | Simulator | [Mujoco](https://github.com/dora-rs/dora-lerobot/blob/main/node-hub/mujoco-client) | 📐 | Mujoco Simulator | | | | ||||
| | Simulator | [Carla](https://github.com/dora-rs/dora-drives) | 📐 | Carla Simulator | | | | | Simulator | [Carla](https://github.com/dora-rs/dora-drives) | 📐 | Carla Simulator | | | | ||||
| | Simulator | [Gymnasium](https://github.com/dora-rs/dora-lerobot/blob/main/gym_dora) | 📐 | Experimental OpenAI Gymnasium bridge | | | | | Simulator | [Gymnasium](https://github.com/dora-rs/dora-lerobot/blob/main/gym_dora) | 📐 | Experimental OpenAI Gymnasium bridge | | | | ||||
| | Image Processing | [Kornia Sobel Operator](https://github.com/kornia/dora-nodes-hub/tree/main/kornia-imgproc-sobel) | ✅ | Kornia image processing Sobel operator (rust) | |  | | |||||
| ## Examples | ## Examples | ||||
| @@ -144,6 +149,7 @@ | |||||
| | Vision | [Vision Language Model(VLM)](https://github.com/dora-rs/dora/blob/main/examples/vlm) | Use a VLM to understand images. |  | | | Vision | [Vision Language Model(VLM)](https://github.com/dora-rs/dora/blob/main/examples/vlm) | Use a VLM to understand images. |  | | ||||
| | Vision | [YOLO](https://github.com/dora-rs/dora/blob/main/examples/python-dataflow) | Use YOLO to detect object within image. |  | | | Vision | [YOLO](https://github.com/dora-rs/dora/blob/main/examples/python-dataflow) | Use YOLO to detect object within image. |  | | ||||
| | Vision | [Camera](https://github.com/dora-rs/dora/blob/main/examples/camera) | Simple webcam plot example |  | | | Vision | [Camera](https://github.com/dora-rs/dora/blob/main/examples/camera) | Simple webcam plot example |  | | ||||
| | Vision | [Image Processing](https://github.com/kornia/kornia-rs/tree/main/examples/dora) | Multi camera image processing | | | |||||
| | Model Training | [Piper RDT](https://github.com/dora-rs/dora/blob/main/examples/piper) | Piper RDT Pipeline |  | | | Model Training | [Piper RDT](https://github.com/dora-rs/dora/blob/main/examples/piper) | Piper RDT Pipeline |  | | ||||
| | Model Training | [LeRobot - Alexander Koch](https://raw.githubusercontent.com/dora-rs/dora-lerobot/refs/heads/main/README.md) | Training Alexander Koch Low Cost Robot with LeRobot |  | | | Model Training | [LeRobot - Alexander Koch](https://raw.githubusercontent.com/dora-rs/dora-lerobot/refs/heads/main/README.md) | Training Alexander Koch Low Cost Robot with LeRobot |  | | ||||
| | ROS2 | [C++ ROS2 Example](https://github.com/dora-rs/dora/blob/main/examples/c++-ros2-dataflow) | Example using C++ ROS2 |  | | | ROS2 | [C++ ROS2 Example](https://github.com/dora-rs/dora/blob/main/examples/c++-ros2-dataflow) | Example using C++ ROS2 |  | | ||||
| @@ -22,11 +22,3 @@ extend-select = [ | |||||
| "D", # pydocstyle | "D", # pydocstyle | ||||
| "UP", | "UP", | ||||
| ] | ] | ||||
| [tool.maturin.target.x86_64-apple-darwin] | |||||
| # macOS deployment target SDK version | |||||
| macos-deployment-target = "14.5" | |||||
| [tool.maturin.target.aarch64-apple-darwin] | |||||
| # macOS deployment target SDK version | |||||
| macos-deployment-target = "14.5" | |||||
| @@ -381,7 +381,7 @@ pub fn resolve_dataflow(dataflow: String) -> eyre::Result<PathBuf> { | |||||
| #[pyfunction] | #[pyfunction] | ||||
| #[pyo3(signature = (dataflow_path, uv=None))] | #[pyo3(signature = (dataflow_path, uv=None))] | ||||
| pub fn run(dataflow_path: String, uv: Option<bool>) -> eyre::Result<()> { | pub fn run(dataflow_path: String, uv: Option<bool>) -> eyre::Result<()> { | ||||
| dora_cli::command::run(dataflow_path, uv.unwrap_or_default()) | |||||
| dora_cli::run_func(dataflow_path, uv.unwrap_or_default()) | |||||
| } | } | ||||
| #[pymodule] | #[pymodule] | ||||
| @@ -11,7 +11,7 @@ fn required_data_size_inner(array: &ArrayData, next_offset: &mut usize) { | |||||
| for (buffer, spec) in array.buffers().iter().zip(&layout.buffers) { | for (buffer, spec) in array.buffers().iter().zip(&layout.buffers) { | ||||
| // consider alignment padding | // consider alignment padding | ||||
| if let BufferSpec::FixedWidth { alignment, .. } = spec { | if let BufferSpec::FixedWidth { alignment, .. } = spec { | ||||
| *next_offset = (*next_offset + alignment - 1) / alignment * alignment; | |||||
| *next_offset = (*next_offset).div_ceil(*alignment) * alignment; | |||||
| } | } | ||||
| *next_offset += buffer.len(); | *next_offset += buffer.len(); | ||||
| } | } | ||||
| @@ -42,7 +42,7 @@ fn copy_array_into_sample_inner( | |||||
| ); | ); | ||||
| // add alignment padding | // add alignment padding | ||||
| if let BufferSpec::FixedWidth { alignment, .. } = spec { | if let BufferSpec::FixedWidth { alignment, .. } = spec { | ||||
| *next_offset = (*next_offset + alignment - 1) / alignment * alignment; | |||||
| *next_offset = (*next_offset).div_ceil(*alignment) * alignment; | |||||
| } | } | ||||
| target_buffer[*next_offset..][..len].copy_from_slice(buffer.as_slice()); | target_buffer[*next_offset..][..len].copy_from_slice(buffer.as_slice()); | ||||
| @@ -44,6 +44,7 @@ mod drop_stream; | |||||
| pub const ZERO_COPY_THRESHOLD: usize = 4096; | pub const ZERO_COPY_THRESHOLD: usize = 4096; | ||||
| #[allow(dead_code)] | |||||
| enum TokioRuntime { | enum TokioRuntime { | ||||
| Runtime(Runtime), | Runtime(Runtime), | ||||
| Handle(Handle), | Handle(Handle), | ||||
| @@ -18,11 +18,3 @@ extend-select = [ | |||||
| "D", # pydocstyle | "D", # pydocstyle | ||||
| "UP", | "UP", | ||||
| ] | ] | ||||
| [tool.maturin.target.x86_64-apple-darwin] | |||||
| # macOS deployment target SDK version | |||||
| macos-deployment-target = "14.5" | |||||
| [tool.maturin.target.aarch64-apple-darwin] | |||||
| # macOS deployment target SDK version | |||||
| macos-deployment-target = "14.5" | |||||
| @@ -5,9 +5,13 @@ use dora_core::{ | |||||
| }; | }; | ||||
| use dora_message::{descriptor::NodeSource, BuildId}; | use dora_message::{descriptor::NodeSource, BuildId}; | ||||
| use eyre::Context; | use eyre::Context; | ||||
| use std::collections::BTreeMap; | |||||
| use std::{collections::BTreeMap, net::IpAddr}; | |||||
| use crate::{connect_to_coordinator, resolve_dataflow, session::DataflowSession}; | |||||
| use super::{default_tracing, Executable}; | |||||
| use crate::{ | |||||
| common::{connect_to_coordinator, local_working_dir, resolve_dataflow}, | |||||
| session::DataflowSession, | |||||
| }; | |||||
| use distributed::{build_distributed_dataflow, wait_until_dataflow_built}; | use distributed::{build_distributed_dataflow, wait_until_dataflow_built}; | ||||
| use local::build_dataflow_locally; | use local::build_dataflow_locally; | ||||
| @@ -16,9 +20,42 @@ mod distributed; | |||||
| mod git; | mod git; | ||||
| mod local; | mod local; | ||||
| #[derive(Debug, clap::Args)] | |||||
| /// Run build commands provided in the given dataflow. | |||||
| pub struct Build { | |||||
| /// Path to the dataflow descriptor file | |||||
| #[clap(value_name = "PATH")] | |||||
| dataflow: String, | |||||
| /// Address of the dora coordinator | |||||
| #[clap(long, value_name = "IP")] | |||||
| coordinator_addr: Option<IpAddr>, | |||||
| /// Port number of the coordinator control server | |||||
| #[clap(long, value_name = "PORT")] | |||||
| coordinator_port: Option<u16>, | |||||
| // Use UV to build nodes. | |||||
| #[clap(long, action)] | |||||
| uv: bool, | |||||
| // Run build on local machine | |||||
| #[clap(long, action)] | |||||
| local: bool, | |||||
| } | |||||
| impl Executable for Build { | |||||
| fn execute(self) -> eyre::Result<()> { | |||||
| default_tracing()?; | |||||
| build( | |||||
| self.dataflow, | |||||
| self.coordinator_addr, | |||||
| self.coordinator_port, | |||||
| self.uv, | |||||
| self.local, | |||||
| ) | |||||
| } | |||||
| } | |||||
| pub fn build( | pub fn build( | ||||
| dataflow: String, | dataflow: String, | ||||
| coordinator_addr: Option<std::net::IpAddr>, | |||||
| coordinator_addr: Option<IpAddr>, | |||||
| coordinator_port: Option<u16>, | coordinator_port: Option<u16>, | ||||
| uv: bool, | uv: bool, | ||||
| force_local: bool, | force_local: bool, | ||||
| @@ -104,7 +141,7 @@ pub fn build( | |||||
| BuildKind::ThroughCoordinator { | BuildKind::ThroughCoordinator { | ||||
| mut coordinator_session, | mut coordinator_session, | ||||
| } => { | } => { | ||||
| let local_working_dir = super::local_working_dir( | |||||
| let local_working_dir = local_working_dir( | |||||
| &dataflow_path, | &dataflow_path, | ||||
| &dataflow_descriptor, | &dataflow_descriptor, | ||||
| &mut *coordinator_session, | &mut *coordinator_session, | ||||
| @@ -1,11 +1,15 @@ | |||||
| use crate::connect_to_coordinator; | |||||
| use super::{default_tracing, Executable}; | |||||
| use crate::{common::connect_to_coordinator, LOCALHOST}; | |||||
| use communication_layer_request_reply::TcpRequestReplyConnection; | use communication_layer_request_reply::TcpRequestReplyConnection; | ||||
| use dora_core::descriptor::DescriptorExt; | |||||
| use dora_core::{descriptor::Descriptor, topics::DORA_COORDINATOR_PORT_CONTROL_DEFAULT}; | |||||
| use dora_message::{cli_to_coordinator::ControlRequest, coordinator_to_cli::ControlRequestReply}; | use dora_message::{cli_to_coordinator::ControlRequest, coordinator_to_cli::ControlRequestReply}; | ||||
| use eyre::{bail, Context}; | use eyre::{bail, Context}; | ||||
| use std::{ | use std::{ | ||||
| io::{IsTerminal, Write}, | io::{IsTerminal, Write}, | ||||
| net::SocketAddr, | net::SocketAddr, | ||||
| }; | }; | ||||
| use std::{net::IpAddr, path::PathBuf}; | |||||
| use termcolor::{Color, ColorChoice, ColorSpec, WriteColor}; | use termcolor::{Color, ColorChoice, ColorSpec, WriteColor}; | ||||
| pub fn check_environment(coordinator_addr: SocketAddr) -> eyre::Result<()> { | pub fn check_environment(coordinator_addr: SocketAddr) -> eyre::Result<()> { | ||||
| @@ -75,3 +79,39 @@ pub fn daemon_running(session: &mut TcpRequestReplyConnection) -> Result<bool, e | |||||
| Ok(running) | Ok(running) | ||||
| } | } | ||||
| #[derive(Debug, clap::Args)] | |||||
| /// Check if the coordinator and the daemon is running. | |||||
| pub struct Check { | |||||
| /// Path to the dataflow descriptor file (enables additional checks) | |||||
| #[clap(long, value_name = "PATH", value_hint = clap::ValueHint::FilePath)] | |||||
| dataflow: Option<PathBuf>, | |||||
| /// Address of the dora coordinator | |||||
| #[clap(long, value_name = "IP", default_value_t = LOCALHOST)] | |||||
| coordinator_addr: IpAddr, | |||||
| /// Port number of the coordinator control server | |||||
| #[clap(long, value_name = "PORT", default_value_t = DORA_COORDINATOR_PORT_CONTROL_DEFAULT)] | |||||
| coordinator_port: u16, | |||||
| } | |||||
| impl Executable for Check { | |||||
| fn execute(self) -> eyre::Result<()> { | |||||
| default_tracing()?; | |||||
| match self.dataflow { | |||||
| Some(dataflow) => { | |||||
| let working_dir = dataflow | |||||
| .canonicalize() | |||||
| .context("failed to canonicalize dataflow path")? | |||||
| .parent() | |||||
| .ok_or_else(|| eyre::eyre!("dataflow path has no parent dir"))? | |||||
| .to_owned(); | |||||
| Descriptor::blocking_read(&dataflow)?.check(&working_dir)?; | |||||
| check_environment((self.coordinator_addr, self.coordinator_port).into())? | |||||
| } | |||||
| None => check_environment((self.coordinator_addr, self.coordinator_port).into())?, | |||||
| } | |||||
| Ok(()) | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,66 @@ | |||||
| use super::Executable; | |||||
| use crate::LISTEN_WILDCARD; | |||||
| use dora_coordinator::Event; | |||||
| use dora_core::topics::{DORA_COORDINATOR_PORT_CONTROL_DEFAULT, DORA_COORDINATOR_PORT_DEFAULT}; | |||||
| #[cfg(feature = "tracing")] | |||||
| use dora_tracing::TracingBuilder; | |||||
| use eyre::Context; | |||||
| use std::net::{IpAddr, SocketAddr}; | |||||
| use tokio::runtime::Builder; | |||||
| use tracing::level_filters::LevelFilter; | |||||
| #[derive(Debug, clap::Args)] | |||||
| /// Run coordinator | |||||
| pub struct Coordinator { | |||||
| /// Network interface to bind to for daemon communication | |||||
| #[clap(long, default_value_t = LISTEN_WILDCARD)] | |||||
| interface: IpAddr, | |||||
| /// Port number to bind to for daemon communication | |||||
| #[clap(long, default_value_t = DORA_COORDINATOR_PORT_DEFAULT)] | |||||
| port: u16, | |||||
| /// Network interface to bind to for control communication | |||||
| #[clap(long, default_value_t = LISTEN_WILDCARD)] | |||||
| control_interface: IpAddr, | |||||
| /// Port number to bind to for control communication | |||||
| #[clap(long, default_value_t = DORA_COORDINATOR_PORT_CONTROL_DEFAULT)] | |||||
| control_port: u16, | |||||
| /// Suppresses all log output to stdout. | |||||
| #[clap(long)] | |||||
| quiet: bool, | |||||
| } | |||||
| impl Executable for Coordinator { | |||||
| fn execute(self) -> eyre::Result<()> { | |||||
| #[cfg(feature = "tracing")] | |||||
| { | |||||
| let name = "dora-coordinator"; | |||||
| let mut builder = TracingBuilder::new(name); | |||||
| if !self.quiet { | |||||
| builder = builder.with_stdout("info"); | |||||
| } | |||||
| builder = builder.with_file(name, LevelFilter::INFO)?; | |||||
| builder | |||||
| .build() | |||||
| .wrap_err("failed to set up tracing subscriber")?; | |||||
| } | |||||
| let rt = Builder::new_multi_thread() | |||||
| .enable_all() | |||||
| .build() | |||||
| .context("tokio runtime failed")?; | |||||
| rt.block_on(async { | |||||
| let bind = SocketAddr::new(self.interface, self.port); | |||||
| let bind_control = SocketAddr::new(self.control_interface, self.control_port); | |||||
| let (port, task) = | |||||
| dora_coordinator::start(bind, bind_control, futures::stream::empty::<Event>()) | |||||
| .await?; | |||||
| if !self.quiet { | |||||
| println!("Listening for incoming daemon connection on {port}"); | |||||
| } | |||||
| task.await | |||||
| }) | |||||
| .context("failed to run dora-coordinator") | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,91 @@ | |||||
| use super::Executable; | |||||
| use crate::{common::handle_dataflow_result, session::DataflowSession}; | |||||
| use dora_core::topics::{ | |||||
| DORA_COORDINATOR_PORT_DEFAULT, DORA_DAEMON_LOCAL_LISTEN_PORT_DEFAULT, LOCALHOST, | |||||
| }; | |||||
| use dora_daemon::LogDestination; | |||||
| #[cfg(feature = "tracing")] | |||||
| use dora_tracing::TracingBuilder; | |||||
| use eyre::Context; | |||||
| use std::{ | |||||
| net::{IpAddr, SocketAddr}, | |||||
| path::PathBuf, | |||||
| }; | |||||
| use tokio::runtime::Builder; | |||||
| use tracing::level_filters::LevelFilter; | |||||
| #[derive(Debug, clap::Args)] | |||||
| /// Run daemon | |||||
| pub struct Daemon { | |||||
| /// Unique identifier for the machine (required for distributed dataflows) | |||||
| #[clap(long)] | |||||
| machine_id: Option<String>, | |||||
| /// Local listen port for event such as dynamic node. | |||||
| #[clap(long, default_value_t = DORA_DAEMON_LOCAL_LISTEN_PORT_DEFAULT)] | |||||
| local_listen_port: u16, | |||||
| /// Address and port number of the dora coordinator | |||||
| #[clap(long, short, default_value_t = LOCALHOST)] | |||||
| coordinator_addr: IpAddr, | |||||
| /// Port number of the coordinator control server | |||||
| #[clap(long, default_value_t = DORA_COORDINATOR_PORT_DEFAULT)] | |||||
| coordinator_port: u16, | |||||
| #[clap(long, hide = true)] | |||||
| run_dataflow: Option<PathBuf>, | |||||
| /// Suppresses all log output to stdout. | |||||
| #[clap(long)] | |||||
| quiet: bool, | |||||
| } | |||||
| impl Executable for Daemon { | |||||
| fn execute(self) -> eyre::Result<()> { | |||||
| #[cfg(feature = "tracing")] | |||||
| { | |||||
| let name = "dora-daemon"; | |||||
| let filename = self | |||||
| .machine_id | |||||
| .as_ref() | |||||
| .map(|id| format!("{name}-{id}")) | |||||
| .unwrap_or(name.to_string()); | |||||
| let mut builder = TracingBuilder::new(name); | |||||
| if !self.quiet { | |||||
| builder = builder.with_stdout("info,zenoh=warn"); | |||||
| } | |||||
| builder = builder.with_file(filename, LevelFilter::INFO)?; | |||||
| builder | |||||
| .build() | |||||
| .wrap_err("failed to set up tracing subscriber")?; | |||||
| } | |||||
| let rt = Builder::new_multi_thread() | |||||
| .enable_all() | |||||
| .build() | |||||
| .context("tokio runtime failed")?; | |||||
| rt.block_on(async { | |||||
| match self.run_dataflow { | |||||
| Some(dataflow_path) => { | |||||
| tracing::info!("Starting dataflow `{}`", dataflow_path.display()); | |||||
| if self.coordinator_addr != LOCALHOST { | |||||
| tracing::info!( | |||||
| "Not using coordinator addr {} as `run_dataflow` is for local dataflow only. Please use the `start` command for remote coordinator", | |||||
| self.coordinator_addr | |||||
| ); | |||||
| } | |||||
| let dataflow_session = | |||||
| DataflowSession::read_session(&dataflow_path).context("failed to read DataflowSession")?; | |||||
| let result = dora_daemon::Daemon::run_dataflow(&dataflow_path, | |||||
| dataflow_session.build_id, dataflow_session.local_build, dataflow_session.session_id, false, | |||||
| LogDestination::Tracing, | |||||
| ).await?; | |||||
| handle_dataflow_result(result, None) | |||||
| } | |||||
| None => { | |||||
| dora_daemon::Daemon::run(SocketAddr::new(self.coordinator_addr, self.coordinator_port), self.machine_id, self.local_listen_port).await | |||||
| } | |||||
| } | |||||
| }) | |||||
| .context("failed to run dora-daemon") | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,28 @@ | |||||
| use super::{default_tracing, up, Executable}; | |||||
| use dora_core::topics::{DORA_COORDINATOR_PORT_CONTROL_DEFAULT, LOCALHOST}; | |||||
| use std::net::IpAddr; | |||||
| use std::path::PathBuf; | |||||
| #[derive(Debug, clap::Args)] | |||||
| /// Destroy running coordinator and daemon. If some dataflows are still running, they will be stopped first. | |||||
| pub struct Destroy { | |||||
| /// Use a custom configuration | |||||
| #[clap(long, hide = true)] | |||||
| config: Option<PathBuf>, | |||||
| /// Address of the dora coordinator | |||||
| #[clap(long, value_name = "IP", default_value_t = LOCALHOST)] | |||||
| coordinator_addr: IpAddr, | |||||
| /// Port number of the coordinator control server | |||||
| #[clap(long, value_name = "PORT", default_value_t = DORA_COORDINATOR_PORT_CONTROL_DEFAULT)] | |||||
| coordinator_port: u16, | |||||
| } | |||||
| impl Executable for Destroy { | |||||
| fn execute(self) -> eyre::Result<()> { | |||||
| default_tracing()?; | |||||
| up::destroy( | |||||
| self.config.as_deref(), | |||||
| (self.coordinator_addr, self.coordinator_port).into(), | |||||
| ) | |||||
| } | |||||
| } | |||||
| @@ -1,11 +1,35 @@ | |||||
| use std::{fs::File, io::Write, path::Path}; | |||||
| use super::Executable; | |||||
| use dora_core::descriptor::{Descriptor, DescriptorExt}; | use dora_core::descriptor::{Descriptor, DescriptorExt}; | ||||
| use eyre::Context; | use eyre::Context; | ||||
| use std::{ | |||||
| fs::File, | |||||
| io::Write, | |||||
| path::{Path, PathBuf}, | |||||
| }; | |||||
| const MERMAID_TEMPLATE: &str = include_str!("graph/mermaid-template.html"); | |||||
| #[derive(Debug, clap::Args)] | |||||
| /// Generate a visualization of the given graph using mermaid.js. Use --open to open browser. | |||||
| pub struct Graph { | |||||
| /// Path to the dataflow descriptor file | |||||
| #[clap(value_name = "PATH", value_hint = clap::ValueHint::FilePath)] | |||||
| dataflow: PathBuf, | |||||
| /// Visualize the dataflow as a Mermaid diagram (instead of HTML) | |||||
| #[clap(long, action)] | |||||
| mermaid: bool, | |||||
| /// Open the HTML visualization in the browser | |||||
| #[clap(long, action)] | |||||
| open: bool, | |||||
| } | |||||
| const MERMAID_TEMPLATE: &str = include_str!("mermaid-template.html"); | |||||
| impl Executable for Graph { | |||||
| fn execute(self) -> eyre::Result<()> { | |||||
| create(self.dataflow, self.mermaid, self.open) | |||||
| } | |||||
| } | |||||
| pub(crate) fn create(dataflow: std::path::PathBuf, mermaid: bool, open: bool) -> eyre::Result<()> { | |||||
| fn create(dataflow: std::path::PathBuf, mermaid: bool, open: bool) -> eyre::Result<()> { | |||||
| if mermaid { | if mermaid { | ||||
| let visualized = visualize_as_mermaid(&dataflow)?; | let visualized = visualize_as_mermaid(&dataflow)?; | ||||
| println!("{visualized}"); | println!("{visualized}"); | ||||
| @@ -0,0 +1 @@ | |||||
| !*template.html | |||||
| @@ -0,0 +1,59 @@ | |||||
| use std::io::Write; | |||||
| use super::{default_tracing, Executable}; | |||||
| use crate::{ | |||||
| common::{connect_to_coordinator, query_running_dataflows}, | |||||
| LOCALHOST, | |||||
| }; | |||||
| use clap::Args; | |||||
| use communication_layer_request_reply::TcpRequestReplyConnection; | |||||
| use dora_core::topics::DORA_COORDINATOR_PORT_CONTROL_DEFAULT; | |||||
| use dora_message::coordinator_to_cli::DataflowStatus; | |||||
| use eyre::eyre; | |||||
| use tabwriter::TabWriter; | |||||
| #[derive(Debug, Args)] | |||||
| /// List running dataflows. | |||||
| pub struct ListArgs { | |||||
| /// Address of the dora coordinator | |||||
| #[clap(long, value_name = "IP", default_value_t = LOCALHOST)] | |||||
| pub coordinator_addr: std::net::IpAddr, | |||||
| /// Port number of the coordinator control server | |||||
| #[clap(long, value_name = "PORT", default_value_t = DORA_COORDINATOR_PORT_CONTROL_DEFAULT)] | |||||
| pub coordinator_port: u16, | |||||
| } | |||||
| impl Executable for ListArgs { | |||||
| fn execute(self) -> eyre::Result<()> { | |||||
| default_tracing()?; | |||||
| let mut session = | |||||
| connect_to_coordinator((self.coordinator_addr, self.coordinator_port).into()) | |||||
| .map_err(|_| eyre!("Failed to connect to coordinator"))?; | |||||
| list(&mut *session) | |||||
| } | |||||
| } | |||||
| fn list(session: &mut TcpRequestReplyConnection) -> Result<(), eyre::ErrReport> { | |||||
| let list = query_running_dataflows(session)?; | |||||
| let mut tw = TabWriter::new(vec![]); | |||||
| tw.write_all(b"UUID\tName\tStatus\n")?; | |||||
| for entry in list.0 { | |||||
| let uuid = entry.id.uuid; | |||||
| let name = entry.id.name.unwrap_or_default(); | |||||
| let status = match entry.status { | |||||
| DataflowStatus::Running => "Running", | |||||
| DataflowStatus::Finished => "Succeeded", | |||||
| DataflowStatus::Failed => "Failed", | |||||
| }; | |||||
| tw.write_all(format!("{uuid}\t{name}\t{status}\n").as_bytes())?; | |||||
| } | |||||
| tw.flush()?; | |||||
| let formatted = String::from_utf8(tw.into_inner()?)?; | |||||
| println!("{formatted}"); | |||||
| Ok(()) | |||||
| } | |||||
| @@ -1,9 +1,54 @@ | |||||
| use super::{default_tracing, Executable}; | |||||
| use crate::common::{connect_to_coordinator, query_running_dataflows}; | |||||
| use bat::{Input, PrettyPrinter}; | |||||
| use clap::Args; | |||||
| use communication_layer_request_reply::TcpRequestReplyConnection; | use communication_layer_request_reply::TcpRequestReplyConnection; | ||||
| use dora_core::topics::{DORA_COORDINATOR_PORT_CONTROL_DEFAULT, LOCALHOST}; | |||||
| use dora_message::{cli_to_coordinator::ControlRequest, coordinator_to_cli::ControlRequestReply}; | use dora_message::{cli_to_coordinator::ControlRequest, coordinator_to_cli::ControlRequestReply}; | ||||
| use eyre::{bail, Context, Result}; | use eyre::{bail, Context, Result}; | ||||
| use uuid::Uuid; | use uuid::Uuid; | ||||
| use bat::{Input, PrettyPrinter}; | |||||
| #[derive(Debug, Args)] | |||||
| /// Show logs of a given dataflow and node. | |||||
| pub struct LogsArgs { | |||||
| /// Identifier of the dataflow | |||||
| #[clap(value_name = "UUID_OR_NAME")] | |||||
| pub dataflow: Option<String>, | |||||
| /// Show logs for the given node | |||||
| #[clap(value_name = "NAME")] | |||||
| pub node: String, | |||||
| /// Address of the dora coordinator | |||||
| #[clap(long, value_name = "IP", default_value_t = LOCALHOST)] | |||||
| pub coordinator_addr: std::net::IpAddr, | |||||
| /// Port number of the coordinator control server | |||||
| #[clap(long, value_name = "PORT", default_value_t = DORA_COORDINATOR_PORT_CONTROL_DEFAULT)] | |||||
| pub coordinator_port: u16, | |||||
| } | |||||
| impl Executable for LogsArgs { | |||||
| fn execute(self) -> eyre::Result<()> { | |||||
| default_tracing()?; | |||||
| let mut session = | |||||
| connect_to_coordinator((self.coordinator_addr, self.coordinator_port).into()) | |||||
| .wrap_err("failed to connect to dora coordinator")?; | |||||
| let list = | |||||
| query_running_dataflows(&mut *session).wrap_err("failed to query running dataflows")?; | |||||
| if let Some(dataflow) = self.dataflow { | |||||
| let uuid = Uuid::parse_str(&dataflow).ok(); | |||||
| let name = if uuid.is_some() { None } else { Some(dataflow) }; | |||||
| logs(&mut *session, uuid, name, self.node) | |||||
| } else { | |||||
| let active = list.get_active(); | |||||
| let uuid = match &active[..] { | |||||
| [] => bail!("No dataflows are running"), | |||||
| [uuid] => uuid.clone(), | |||||
| _ => inquire::Select::new("Choose dataflow to show logs:", active).prompt()?, | |||||
| }; | |||||
| logs(&mut *session, Some(uuid.uuid), None, self.node) | |||||
| } | |||||
| } | |||||
| } | |||||
| pub fn logs( | pub fn logs( | ||||
| session: &mut TcpRequestReplyConnection, | session: &mut TcpRequestReplyConnection, | ||||
| @@ -1,60 +1,104 @@ | |||||
| pub use build::build; | |||||
| pub use logs::logs; | |||||
| pub use run::run; | |||||
| pub use start::start; | |||||
| use std::path::{Path, PathBuf}; | |||||
| use communication_layer_request_reply::TcpRequestReplyConnection; | |||||
| use dora_core::descriptor::Descriptor; | |||||
| use dora_message::{cli_to_coordinator::ControlRequest, coordinator_to_cli::ControlRequestReply}; | |||||
| use eyre::{bail, Context, ContextCompat}; | |||||
| mod build; | mod build; | ||||
| pub mod check; | |||||
| mod check; | |||||
| mod coordinator; | |||||
| mod daemon; | |||||
| mod destroy; | |||||
| mod graph; | |||||
| mod list; | |||||
| mod logs; | mod logs; | ||||
| mod new; | |||||
| mod run; | mod run; | ||||
| mod runtime; | |||||
| mod self_; | |||||
| mod start; | mod start; | ||||
| pub mod up; | |||||
| fn local_working_dir( | |||||
| dataflow_path: &Path, | |||||
| dataflow_descriptor: &Descriptor, | |||||
| coordinator_session: &mut TcpRequestReplyConnection, | |||||
| ) -> eyre::Result<Option<PathBuf>> { | |||||
| Ok( | |||||
| if dataflow_descriptor | |||||
| .nodes | |||||
| .iter() | |||||
| .all(|n| n.deploy.as_ref().map(|d| d.machine.as_ref()).is_none()) | |||||
| && cli_and_daemon_on_same_machine(coordinator_session)? | |||||
| { | |||||
| Some( | |||||
| dunce::canonicalize(dataflow_path) | |||||
| .context("failed to canonicalize dataflow file path")? | |||||
| .parent() | |||||
| .context("dataflow path has no parent dir")? | |||||
| .to_owned(), | |||||
| ) | |||||
| } else { | |||||
| None | |||||
| }, | |||||
| ) | |||||
| mod stop; | |||||
| mod up; | |||||
| pub use run::run_func; | |||||
| use build::Build; | |||||
| use check::Check; | |||||
| use coordinator::Coordinator; | |||||
| use daemon::Daemon; | |||||
| use destroy::Destroy; | |||||
| use eyre::Context; | |||||
| use graph::Graph; | |||||
| use list::ListArgs; | |||||
| use logs::LogsArgs; | |||||
| use new::NewArgs; | |||||
| use run::Run; | |||||
| use runtime::Runtime; | |||||
| use self_::SelfSubCommand; | |||||
| use start::Start; | |||||
| use stop::Stop; | |||||
| use up::Up; | |||||
| /// dora-rs cli client | |||||
| #[derive(Debug, clap::Subcommand)] | |||||
| pub enum Command { | |||||
| Check(Check), | |||||
| Graph(Graph), | |||||
| Build(Build), | |||||
| New(NewArgs), | |||||
| Run(Run), | |||||
| Up(Up), | |||||
| Destroy(Destroy), | |||||
| Start(Start), | |||||
| Stop(Stop), | |||||
| List(ListArgs), | |||||
| // Planned for future releases: | |||||
| // Dashboard, | |||||
| #[command(allow_missing_positional = true)] | |||||
| Logs(LogsArgs), | |||||
| // Metrics, | |||||
| // Stats, | |||||
| // Get, | |||||
| // Upgrade, | |||||
| Daemon(Daemon), | |||||
| Runtime(Runtime), | |||||
| Coordinator(Coordinator), | |||||
| Self_ { | |||||
| #[clap(subcommand)] | |||||
| command: SelfSubCommand, | |||||
| }, | |||||
| } | |||||
| fn default_tracing() -> eyre::Result<()> { | |||||
| #[cfg(feature = "tracing")] | |||||
| { | |||||
| use dora_tracing::TracingBuilder; | |||||
| TracingBuilder::new("dora-cli") | |||||
| .with_stdout("warn") | |||||
| .build() | |||||
| .wrap_err("failed to set up tracing subscriber")?; | |||||
| } | |||||
| Ok(()) | |||||
| } | |||||
| pub trait Executable { | |||||
| fn execute(self) -> eyre::Result<()>; | |||||
| } | } | ||||
| fn cli_and_daemon_on_same_machine(session: &mut TcpRequestReplyConnection) -> eyre::Result<bool> { | |||||
| let reply_raw = session | |||||
| .request(&serde_json::to_vec(&ControlRequest::CliAndDefaultDaemonOnSameMachine).unwrap()) | |||||
| .wrap_err("failed to send start dataflow message")?; | |||||
| let result: ControlRequestReply = | |||||
| serde_json::from_slice(&reply_raw).wrap_err("failed to parse reply")?; | |||||
| match result { | |||||
| ControlRequestReply::CliAndDefaultDaemonIps { | |||||
| default_daemon, | |||||
| cli, | |||||
| } => Ok(default_daemon.is_some() && default_daemon == cli), | |||||
| ControlRequestReply::Error(err) => bail!("{err}"), | |||||
| other => bail!("unexpected start dataflow reply: {other:?}"), | |||||
| impl Executable for Command { | |||||
| fn execute(self) -> eyre::Result<()> { | |||||
| match self { | |||||
| Command::Check(args) => args.execute(), | |||||
| Command::Coordinator(args) => args.execute(), | |||||
| Command::Graph(args) => args.execute(), | |||||
| Command::Build(args) => args.execute(), | |||||
| Command::New(args) => args.execute(), | |||||
| Command::Run(args) => args.execute(), | |||||
| Command::Up(args) => args.execute(), | |||||
| Command::Destroy(args) => args.execute(), | |||||
| Command::Start(args) => args.execute(), | |||||
| Command::Stop(args) => args.execute(), | |||||
| Command::List(args) => args.execute(), | |||||
| Command::Logs(args) => args.execute(), | |||||
| Command::Daemon(args) => args.execute(), | |||||
| Command::Self_ { command } => command.execute(), | |||||
| Command::Runtime(args) => args.execute(), | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -0,0 +1,21 @@ | |||||
| use clap::Args; | |||||
| use super::{default_tracing, Executable}; | |||||
| #[derive(Debug, Args)] | |||||
| /// Generate a new project or node. Choose the language between Rust, Python, C or C++. | |||||
| pub struct NewArgs { | |||||
| #[clap(flatten)] | |||||
| // TODO!: better impl | |||||
| args: crate::CommandNew, | |||||
| /// Internal flag for creating with path dependencies | |||||
| #[clap(hide = true, long)] | |||||
| pub internal_create_with_path_dependencies: bool, | |||||
| } | |||||
| impl Executable for NewArgs { | |||||
| fn execute(self) -> eyre::Result<()> { | |||||
| default_tracing()?; | |||||
| crate::template::create(self.args, self.internal_create_with_path_dependencies) | |||||
| } | |||||
| } | |||||
| @@ -1,12 +1,38 @@ | |||||
| use super::Executable; | |||||
| use crate::{ | |||||
| common::{handle_dataflow_result, resolve_dataflow}, | |||||
| output::print_log_message, | |||||
| session::DataflowSession, | |||||
| }; | |||||
| use dora_daemon::{flume, Daemon, LogDestination}; | use dora_daemon::{flume, Daemon, LogDestination}; | ||||
| use dora_tracing::TracingBuilder; | |||||
| use eyre::Context; | use eyre::Context; | ||||
| use tokio::runtime::Builder; | use tokio::runtime::Builder; | ||||
| use crate::{ | |||||
| handle_dataflow_result, output::print_log_message, resolve_dataflow, session::DataflowSession, | |||||
| }; | |||||
| #[derive(Debug, clap::Args)] | |||||
| /// Run a dataflow locally. | |||||
| /// | |||||
| /// Directly runs the given dataflow without connecting to a dora | |||||
| /// coordinator or daemon. The dataflow is executed on the local machine. | |||||
| pub struct Run { | |||||
| /// Path to the dataflow descriptor file | |||||
| #[clap(value_name = "PATH")] | |||||
| dataflow: String, | |||||
| // Use UV to run nodes. | |||||
| #[clap(long, action)] | |||||
| uv: bool, | |||||
| } | |||||
| pub fn run_func(dataflow: String, uv: bool) -> eyre::Result<()> { | |||||
| #[cfg(feature = "tracing")] | |||||
| { | |||||
| let log_level = std::env::var("RUST_LOG").ok().unwrap_or("info".to_string()); | |||||
| TracingBuilder::new("run") | |||||
| .with_stdout(log_level) | |||||
| .build() | |||||
| .wrap_err("failed to set up tracing subscriber")?; | |||||
| } | |||||
| pub fn run(dataflow: String, uv: bool) -> Result<(), eyre::Error> { | |||||
| let dataflow_path = resolve_dataflow(dataflow).context("could not resolve dataflow")?; | let dataflow_path = resolve_dataflow(dataflow).context("could not resolve dataflow")?; | ||||
| let dataflow_session = | let dataflow_session = | ||||
| DataflowSession::read_session(&dataflow_path).context("failed to read DataflowSession")?; | DataflowSession::read_session(&dataflow_path).context("failed to read DataflowSession")?; | ||||
| @@ -32,3 +58,9 @@ pub fn run(dataflow: String, uv: bool) -> Result<(), eyre::Error> { | |||||
| ))?; | ))?; | ||||
| handle_dataflow_result(result, None) | handle_dataflow_result(result, None) | ||||
| } | } | ||||
| impl Executable for Run { | |||||
| fn execute(self) -> eyre::Result<()> { | |||||
| run_func(self.dataflow, self.uv) | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,15 @@ | |||||
| use eyre::Context; | |||||
| use super::Executable; | |||||
| #[derive(Debug, clap::Args)] | |||||
| /// Run runtime | |||||
| pub struct Runtime; | |||||
| impl Executable for Runtime { | |||||
| fn execute(self) -> eyre::Result<()> { | |||||
| // No tracing: Do not set the runtime in the cli. | |||||
| // ref: 72b4be808122574fcfda69650954318e0355cc7b cli::run | |||||
| dora_runtime::main().context("Failed to run dora-runtime") | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,139 @@ | |||||
| use super::{default_tracing, Executable}; | |||||
| use clap::Subcommand; | |||||
| use eyre::{bail, Context}; | |||||
| #[derive(Debug, Subcommand)] | |||||
| /// Dora CLI self-management commands | |||||
| pub enum SelfSubCommand { | |||||
| /// Check for updates or update the CLI | |||||
| Update { | |||||
| /// Only check for updates without installing | |||||
| #[clap(long)] | |||||
| check_only: bool, | |||||
| }, | |||||
| /// Remove The Dora CLI from the system | |||||
| Uninstall { | |||||
| /// Force uninstallation without confirmation | |||||
| #[clap(long)] | |||||
| force: bool, | |||||
| }, | |||||
| } | |||||
| impl Executable for SelfSubCommand { | |||||
| fn execute(self) -> eyre::Result<()> { | |||||
| default_tracing()?; | |||||
| match self { | |||||
| SelfSubCommand::Update { check_only } => { | |||||
| println!("Checking for updates..."); | |||||
| #[cfg(target_os = "linux")] | |||||
| let bin_path_in_archive = format!("dora-cli-{}/dora", env!("TARGET")); | |||||
| #[cfg(target_os = "macos")] | |||||
| let bin_path_in_archive = format!("dora-cli-{}/dora", env!("TARGET")); | |||||
| #[cfg(target_os = "windows")] | |||||
| let bin_path_in_archive = String::from("dora.exe"); | |||||
| let status = self_update::backends::github::Update::configure() | |||||
| .repo_owner("dora-rs") | |||||
| .repo_name("dora") | |||||
| .bin_path_in_archive(&bin_path_in_archive) | |||||
| .bin_name("dora") | |||||
| .show_download_progress(true) | |||||
| .current_version(env!("CARGO_PKG_VERSION")) | |||||
| .build()?; | |||||
| if check_only { | |||||
| // Only check if an update is available | |||||
| match status.get_latest_release() { | |||||
| Ok(release) => { | |||||
| let current_version = self_update::cargo_crate_version!(); | |||||
| if current_version != release.version { | |||||
| println!( | |||||
| "An update is available: {}. Run 'dora self update' to update", | |||||
| release.version | |||||
| ); | |||||
| } else { | |||||
| println!( | |||||
| "Dora CLI is already at the latest version: {}", | |||||
| current_version | |||||
| ); | |||||
| } | |||||
| } | |||||
| Err(e) => println!("Failed to check for updates: {}", e), | |||||
| } | |||||
| } else { | |||||
| // Perform the actual update | |||||
| match status.update() { | |||||
| Ok(update_status) => match update_status { | |||||
| self_update::Status::UpToDate(version) => { | |||||
| println!("Dora CLI is already at the latest version: {}", version); | |||||
| } | |||||
| self_update::Status::Updated(version) => { | |||||
| println!("Successfully updated Dora CLI to version: {}", version); | |||||
| } | |||||
| }, | |||||
| Err(e) => println!("Failed to update: {}", e), | |||||
| } | |||||
| } | |||||
| } | |||||
| SelfSubCommand::Uninstall { force } => { | |||||
| if !force { | |||||
| let confirmed = | |||||
| inquire::Confirm::new("Are you sure you want to uninstall Dora CLI?") | |||||
| .with_default(false) | |||||
| .prompt() | |||||
| .wrap_err("Uninstallation cancelled")?; | |||||
| if !confirmed { | |||||
| println!("Uninstallation cancelled"); | |||||
| return Ok(()); | |||||
| } | |||||
| } | |||||
| println!("Uninstalling Dora CLI..."); | |||||
| #[cfg(feature = "python")] | |||||
| { | |||||
| println!("Detected Python installation..."); | |||||
| // Try uv pip uninstall first | |||||
| let uv_status = std::process::Command::new("uv") | |||||
| .args(["pip", "uninstall", "dora-rs-cli"]) | |||||
| .status(); | |||||
| if let Ok(status) = uv_status { | |||||
| if status.success() { | |||||
| println!("Dora CLI has been successfully uninstalled via uv pip."); | |||||
| return Ok(()); | |||||
| } | |||||
| } | |||||
| // Fall back to regular pip uninstall | |||||
| println!("Trying with pip..."); | |||||
| let status = std::process::Command::new("pip") | |||||
| .args(["uninstall", "-y", "dora-rs-cli"]) | |||||
| .status() | |||||
| .wrap_err("Failed to run pip uninstall")?; | |||||
| if status.success() { | |||||
| println!("Dora CLI has been successfully uninstalled via pip."); | |||||
| } else { | |||||
| bail!("Failed to uninstall Dora CLI via pip."); | |||||
| } | |||||
| } | |||||
| #[cfg(not(feature = "python"))] | |||||
| { | |||||
| match self_replace::self_delete() { | |||||
| Ok(_) => { | |||||
| println!("Dora CLI has been successfully uninstalled."); | |||||
| } | |||||
| Err(e) => { | |||||
| bail!("Failed to uninstall Dora CLI: {}", e); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| Ok(()) | |||||
| } | |||||
| } | |||||
| @@ -14,7 +14,7 @@ use std::{path::PathBuf, sync::mpsc, time::Duration}; | |||||
| use tracing::{error, info}; | use tracing::{error, info}; | ||||
| use uuid::Uuid; | use uuid::Uuid; | ||||
| use crate::handle_dataflow_result; | |||||
| use crate::common::handle_dataflow_result; | |||||
| use crate::output::print_log_message; | use crate::output::print_log_message; | ||||
| pub fn attach_dataflow( | pub fn attach_dataflow( | ||||
| @@ -1,70 +1,101 @@ | |||||
| use super::{default_tracing, Executable}; | |||||
| use crate::{ | |||||
| command::start::attach::attach_dataflow, | |||||
| common::{connect_to_coordinator, local_working_dir, resolve_dataflow}, | |||||
| output::print_log_message, | |||||
| session::DataflowSession, | |||||
| }; | |||||
| use communication_layer_request_reply::{TcpConnection, TcpRequestReplyConnection}; | use communication_layer_request_reply::{TcpConnection, TcpRequestReplyConnection}; | ||||
| use dora_core::descriptor::{Descriptor, DescriptorExt}; | |||||
| use dora_core::{ | |||||
| descriptor::{Descriptor, DescriptorExt}, | |||||
| topics::{DORA_COORDINATOR_PORT_CONTROL_DEFAULT, LOCALHOST}, | |||||
| }; | |||||
| use dora_message::{ | use dora_message::{ | ||||
| cli_to_coordinator::ControlRequest, common::LogMessage, coordinator_to_cli::ControlRequestReply, | cli_to_coordinator::ControlRequest, common::LogMessage, coordinator_to_cli::ControlRequestReply, | ||||
| }; | }; | ||||
| use eyre::{bail, Context}; | use eyre::{bail, Context}; | ||||
| use std::{ | use std::{ | ||||
| net::{SocketAddr, TcpStream}, | |||||
| net::{IpAddr, SocketAddr, TcpStream}, | |||||
| path::PathBuf, | path::PathBuf, | ||||
| }; | }; | ||||
| use uuid::Uuid; | use uuid::Uuid; | ||||
| use crate::{ | |||||
| connect_to_coordinator, output::print_log_message, resolve_dataflow, session::DataflowSession, | |||||
| }; | |||||
| use attach::attach_dataflow; | |||||
| mod attach; | mod attach; | ||||
| pub fn start( | |||||
| #[derive(Debug, clap::Args)] | |||||
| /// Start the given dataflow path. Attach a name to the running dataflow by using --name. | |||||
| pub struct Start { | |||||
| /// Path to the dataflow descriptor file | |||||
| #[clap(value_name = "PATH")] | |||||
| dataflow: String, | dataflow: String, | ||||
| /// Assign a name to the dataflow | |||||
| #[clap(long)] | |||||
| name: Option<String>, | name: Option<String>, | ||||
| coordinator_socket: SocketAddr, | |||||
| /// Address of the dora coordinator | |||||
| #[clap(long, value_name = "IP", default_value_t = LOCALHOST)] | |||||
| coordinator_addr: IpAddr, | |||||
| /// Port number of the coordinator control server | |||||
| #[clap(long, value_name = "PORT", default_value_t = DORA_COORDINATOR_PORT_CONTROL_DEFAULT)] | |||||
| coordinator_port: u16, | |||||
| /// Attach to the dataflow and wait for its completion | |||||
| #[clap(long, action)] | |||||
| attach: bool, | attach: bool, | ||||
| /// Run the dataflow in background | |||||
| #[clap(long, action)] | |||||
| detach: bool, | detach: bool, | ||||
| /// Enable hot reloading (Python only) | |||||
| #[clap(long, action)] | |||||
| hot_reload: bool, | hot_reload: bool, | ||||
| // Use UV to run nodes. | |||||
| #[clap(long, action)] | |||||
| uv: bool, | uv: bool, | ||||
| ) -> eyre::Result<()> { | |||||
| let (dataflow, dataflow_descriptor, mut session, dataflow_id) = | |||||
| start_dataflow(dataflow, name, coordinator_socket, uv)?; | |||||
| } | |||||
| let attach = match (attach, detach) { | |||||
| (true, true) => eyre::bail!("both `--attach` and `--detach` are given"), | |||||
| (true, false) => true, | |||||
| (false, true) => false, | |||||
| (false, false) => { | |||||
| println!("attaching to dataflow (use `--detach` to run in background)"); | |||||
| true | |||||
| } | |||||
| }; | |||||
| impl Executable for Start { | |||||
| fn execute(self) -> eyre::Result<()> { | |||||
| default_tracing()?; | |||||
| let coordinator_socket = (self.coordinator_addr, self.coordinator_port).into(); | |||||
| if attach { | |||||
| let log_level = env_logger::Builder::new() | |||||
| .filter_level(log::LevelFilter::Info) | |||||
| .parse_default_env() | |||||
| .build() | |||||
| .filter(); | |||||
| let (dataflow, dataflow_descriptor, mut session, dataflow_id) = | |||||
| start_dataflow(self.dataflow, self.name, coordinator_socket, self.uv)?; | |||||
| attach_dataflow( | |||||
| dataflow_descriptor, | |||||
| dataflow, | |||||
| dataflow_id, | |||||
| &mut *session, | |||||
| hot_reload, | |||||
| coordinator_socket, | |||||
| log_level, | |||||
| ) | |||||
| } else { | |||||
| let print_daemon_name = dataflow_descriptor.nodes.iter().any(|n| n.deploy.is_some()); | |||||
| // wait until dataflow is started | |||||
| wait_until_dataflow_started( | |||||
| dataflow_id, | |||||
| &mut session, | |||||
| coordinator_socket, | |||||
| log::LevelFilter::Info, | |||||
| print_daemon_name, | |||||
| ) | |||||
| let attach = match (self.attach, self.detach) { | |||||
| (true, true) => eyre::bail!("both `--attach` and `--detach` are given"), | |||||
| (true, false) => true, | |||||
| (false, true) => false, | |||||
| (false, false) => { | |||||
| println!("attaching to dataflow (use `--detach` to run in background)"); | |||||
| true | |||||
| } | |||||
| }; | |||||
| if attach { | |||||
| let log_level = env_logger::Builder::new() | |||||
| .filter_level(log::LevelFilter::Info) | |||||
| .parse_default_env() | |||||
| .build() | |||||
| .filter(); | |||||
| attach_dataflow( | |||||
| dataflow_descriptor, | |||||
| dataflow, | |||||
| dataflow_id, | |||||
| &mut *session, | |||||
| self.hot_reload, | |||||
| coordinator_socket, | |||||
| log_level, | |||||
| ) | |||||
| } else { | |||||
| let print_daemon_name = dataflow_descriptor.nodes.iter().any(|n| n.deploy.is_some()); | |||||
| // wait until dataflow is started | |||||
| wait_until_dataflow_started( | |||||
| dataflow_id, | |||||
| &mut session, | |||||
| coordinator_socket, | |||||
| log::LevelFilter::Info, | |||||
| print_daemon_name, | |||||
| ) | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -83,8 +114,7 @@ fn start_dataflow( | |||||
| let mut session = connect_to_coordinator(coordinator_socket) | let mut session = connect_to_coordinator(coordinator_socket) | ||||
| .wrap_err("failed to connect to dora coordinator")?; | .wrap_err("failed to connect to dora coordinator")?; | ||||
| let local_working_dir = | |||||
| super::local_working_dir(&dataflow, &dataflow_descriptor, &mut *session)?; | |||||
| let local_working_dir = local_working_dir(&dataflow, &dataflow_descriptor, &mut *session)?; | |||||
| let dataflow_id = { | let dataflow_id = { | ||||
| let dataflow = dataflow_descriptor.clone(); | let dataflow = dataflow_descriptor.clone(); | ||||
| @@ -0,0 +1,111 @@ | |||||
| use super::{default_tracing, Executable}; | |||||
| use crate::common::{connect_to_coordinator, handle_dataflow_result, query_running_dataflows}; | |||||
| use communication_layer_request_reply::TcpRequestReplyConnection; | |||||
| use dora_core::topics::{DORA_COORDINATOR_PORT_CONTROL_DEFAULT, LOCALHOST}; | |||||
| use dora_message::cli_to_coordinator::ControlRequest; | |||||
| use dora_message::coordinator_to_cli::ControlRequestReply; | |||||
| use duration_str::parse; | |||||
| use eyre::{bail, Context}; | |||||
| use std::net::IpAddr; | |||||
| use std::time::Duration; | |||||
| use uuid::Uuid; | |||||
| #[derive(Debug, clap::Args)] | |||||
| /// Stop the given dataflow UUID. If no id is provided, you will be able to choose between the running dataflows. | |||||
| pub struct Stop { | |||||
| /// UUID of the dataflow that should be stopped | |||||
| uuid: Option<Uuid>, | |||||
| /// Name of the dataflow that should be stopped | |||||
| #[clap(long)] | |||||
| name: Option<String>, | |||||
| /// Kill the dataflow if it doesn't stop after the given duration | |||||
| #[clap(long, value_name = "DURATION")] | |||||
| #[arg(value_parser = parse)] | |||||
| grace_duration: Option<Duration>, | |||||
| /// Address of the dora coordinator | |||||
| #[clap(long, value_name = "IP", default_value_t = LOCALHOST)] | |||||
| coordinator_addr: IpAddr, | |||||
| /// Port number of the coordinator control server | |||||
| #[clap(long, value_name = "PORT", default_value_t = DORA_COORDINATOR_PORT_CONTROL_DEFAULT)] | |||||
| coordinator_port: u16, | |||||
| } | |||||
| impl Executable for Stop { | |||||
| fn execute(self) -> eyre::Result<()> { | |||||
| default_tracing()?; | |||||
| let mut session = | |||||
| connect_to_coordinator((self.coordinator_addr, self.coordinator_port).into()) | |||||
| .wrap_err("could not connect to dora coordinator")?; | |||||
| match (self.uuid, self.name) { | |||||
| (Some(uuid), _) => stop_dataflow(uuid, self.grace_duration, &mut *session), | |||||
| (None, Some(name)) => stop_dataflow_by_name(name, self.grace_duration, &mut *session), | |||||
| (None, None) => stop_dataflow_interactive(self.grace_duration, &mut *session), | |||||
| } | |||||
| } | |||||
| } | |||||
| fn stop_dataflow_interactive( | |||||
| grace_duration: Option<Duration>, | |||||
| session: &mut TcpRequestReplyConnection, | |||||
| ) -> eyre::Result<()> { | |||||
| let list = query_running_dataflows(session).wrap_err("failed to query running dataflows")?; | |||||
| let active = list.get_active(); | |||||
| if active.is_empty() { | |||||
| eprintln!("No dataflows are running"); | |||||
| } else { | |||||
| let selection = inquire::Select::new("Choose dataflow to stop:", active).prompt()?; | |||||
| stop_dataflow(selection.uuid, grace_duration, session)?; | |||||
| } | |||||
| Ok(()) | |||||
| } | |||||
| fn stop_dataflow( | |||||
| uuid: Uuid, | |||||
| grace_duration: Option<Duration>, | |||||
| session: &mut TcpRequestReplyConnection, | |||||
| ) -> Result<(), eyre::ErrReport> { | |||||
| let reply_raw = session | |||||
| .request( | |||||
| &serde_json::to_vec(&ControlRequest::Stop { | |||||
| dataflow_uuid: uuid, | |||||
| grace_duration, | |||||
| }) | |||||
| .unwrap(), | |||||
| ) | |||||
| .wrap_err("failed to send dataflow stop message")?; | |||||
| let result: ControlRequestReply = | |||||
| serde_json::from_slice(&reply_raw).wrap_err("failed to parse reply")?; | |||||
| match result { | |||||
| ControlRequestReply::DataflowStopped { uuid, result } => { | |||||
| handle_dataflow_result(result, Some(uuid)) | |||||
| } | |||||
| ControlRequestReply::Error(err) => bail!("{err}"), | |||||
| other => bail!("unexpected stop dataflow reply: {other:?}"), | |||||
| } | |||||
| } | |||||
| fn stop_dataflow_by_name( | |||||
| name: String, | |||||
| grace_duration: Option<Duration>, | |||||
| session: &mut TcpRequestReplyConnection, | |||||
| ) -> Result<(), eyre::ErrReport> { | |||||
| let reply_raw = session | |||||
| .request( | |||||
| &serde_json::to_vec(&ControlRequest::StopByName { | |||||
| name, | |||||
| grace_duration, | |||||
| }) | |||||
| .unwrap(), | |||||
| ) | |||||
| .wrap_err("failed to send dataflow stop_by_name message")?; | |||||
| let result: ControlRequestReply = | |||||
| serde_json::from_slice(&reply_raw).wrap_err("failed to parse reply")?; | |||||
| match result { | |||||
| ControlRequestReply::DataflowStopped { uuid, result } => { | |||||
| handle_dataflow_result(result, Some(uuid)) | |||||
| } | |||||
| ControlRequestReply::Error(err) => bail!("{err}"), | |||||
| other => bail!("unexpected stop dataflow reply: {other:?}"), | |||||
| } | |||||
| } | |||||
| @@ -1,8 +1,27 @@ | |||||
| use crate::{command::check::daemon_running, connect_to_coordinator, LOCALHOST}; | |||||
| use super::check::daemon_running; | |||||
| use super::{default_tracing, Executable}; | |||||
| use crate::{common::connect_to_coordinator, LOCALHOST}; | |||||
| use dora_core::topics::DORA_COORDINATOR_PORT_CONTROL_DEFAULT; | use dora_core::topics::DORA_COORDINATOR_PORT_CONTROL_DEFAULT; | ||||
| use dora_message::{cli_to_coordinator::ControlRequest, coordinator_to_cli::ControlRequestReply}; | use dora_message::{cli_to_coordinator::ControlRequest, coordinator_to_cli::ControlRequestReply}; | ||||
| use eyre::{bail, Context, ContextCompat}; | use eyre::{bail, Context, ContextCompat}; | ||||
| use std::path::PathBuf; | |||||
| use std::{fs, net::SocketAddr, path::Path, process::Command, time::Duration}; | use std::{fs, net::SocketAddr, path::Path, process::Command, time::Duration}; | ||||
| #[derive(Debug, clap::Args)] | |||||
| /// Spawn coordinator and daemon in local mode (with default config) | |||||
| pub struct Up { | |||||
| /// Use a custom configuration | |||||
| #[clap(long, hide = true, value_name = "PATH", value_hint = clap::ValueHint::FilePath)] | |||||
| config: Option<PathBuf>, | |||||
| } | |||||
| impl Executable for Up { | |||||
| fn execute(self) -> eyre::Result<()> { | |||||
| default_tracing()?; | |||||
| up(self.config.as_deref()) | |||||
| } | |||||
| } | |||||
| #[derive(Debug, Default, serde::Serialize, serde::Deserialize)] | #[derive(Debug, Default, serde::Serialize, serde::Deserialize)] | ||||
| struct UpConfig {} | struct UpConfig {} | ||||
| @@ -0,0 +1,117 @@ | |||||
| use crate::formatting::FormatDataflowError; | |||||
| use communication_layer_request_reply::{RequestReplyLayer, TcpLayer, TcpRequestReplyConnection}; | |||||
| use dora_core::descriptor::{source_is_url, Descriptor}; | |||||
| use dora_download::download_file; | |||||
| use dora_message::{ | |||||
| cli_to_coordinator::ControlRequest, | |||||
| coordinator_to_cli::{ControlRequestReply, DataflowList, DataflowResult}, | |||||
| }; | |||||
| use eyre::{bail, Context, ContextCompat}; | |||||
| use std::{ | |||||
| env::current_dir, | |||||
| net::SocketAddr, | |||||
| path::{Path, PathBuf}, | |||||
| }; | |||||
| use tokio::runtime::Builder; | |||||
| use uuid::Uuid; | |||||
| pub(crate) fn handle_dataflow_result( | |||||
| result: DataflowResult, | |||||
| uuid: Option<Uuid>, | |||||
| ) -> Result<(), eyre::Error> { | |||||
| if result.is_ok() { | |||||
| Ok(()) | |||||
| } else { | |||||
| Err(match uuid { | |||||
| Some(uuid) => { | |||||
| eyre::eyre!("Dataflow {uuid} failed:\n{}", FormatDataflowError(&result)) | |||||
| } | |||||
| None => { | |||||
| eyre::eyre!("Dataflow failed:\n{}", FormatDataflowError(&result)) | |||||
| } | |||||
| }) | |||||
| } | |||||
| } | |||||
| pub(crate) fn query_running_dataflows( | |||||
| session: &mut TcpRequestReplyConnection, | |||||
| ) -> eyre::Result<DataflowList> { | |||||
| let reply_raw = session | |||||
| .request(&serde_json::to_vec(&ControlRequest::List).unwrap()) | |||||
| .wrap_err("failed to send list message")?; | |||||
| let reply: ControlRequestReply = | |||||
| serde_json::from_slice(&reply_raw).wrap_err("failed to parse reply")?; | |||||
| let ids = match reply { | |||||
| ControlRequestReply::DataflowList(list) => list, | |||||
| ControlRequestReply::Error(err) => bail!("{err}"), | |||||
| other => bail!("unexpected list dataflow reply: {other:?}"), | |||||
| }; | |||||
| Ok(ids) | |||||
| } | |||||
| pub(crate) fn connect_to_coordinator( | |||||
| coordinator_addr: SocketAddr, | |||||
| ) -> std::io::Result<Box<TcpRequestReplyConnection>> { | |||||
| TcpLayer::new().connect(coordinator_addr) | |||||
| } | |||||
| pub(crate) fn resolve_dataflow(dataflow: String) -> eyre::Result<PathBuf> { | |||||
| let dataflow = if source_is_url(&dataflow) { | |||||
| // try to download the shared library | |||||
| let target_path = current_dir().context("Could not access the current dir")?; | |||||
| let rt = Builder::new_current_thread() | |||||
| .enable_all() | |||||
| .build() | |||||
| .context("tokio runtime failed")?; | |||||
| rt.block_on(async { download_file(&dataflow, &target_path).await }) | |||||
| .wrap_err("failed to download dataflow yaml file")? | |||||
| } else { | |||||
| PathBuf::from(dataflow) | |||||
| }; | |||||
| Ok(dataflow) | |||||
| } | |||||
| pub(crate) fn local_working_dir( | |||||
| dataflow_path: &Path, | |||||
| dataflow_descriptor: &Descriptor, | |||||
| coordinator_session: &mut TcpRequestReplyConnection, | |||||
| ) -> eyre::Result<Option<PathBuf>> { | |||||
| Ok( | |||||
| if dataflow_descriptor | |||||
| .nodes | |||||
| .iter() | |||||
| .all(|n| n.deploy.as_ref().map(|d| d.machine.as_ref()).is_none()) | |||||
| && cli_and_daemon_on_same_machine(coordinator_session)? | |||||
| { | |||||
| Some( | |||||
| dunce::canonicalize(dataflow_path) | |||||
| .context("failed to canonicalize dataflow file path")? | |||||
| .parent() | |||||
| .context("dataflow path has no parent dir")? | |||||
| .to_owned(), | |||||
| ) | |||||
| } else { | |||||
| None | |||||
| }, | |||||
| ) | |||||
| } | |||||
| pub(crate) fn cli_and_daemon_on_same_machine( | |||||
| session: &mut TcpRequestReplyConnection, | |||||
| ) -> eyre::Result<bool> { | |||||
| let reply_raw = session | |||||
| .request(&serde_json::to_vec(&ControlRequest::CliAndDefaultDaemonOnSameMachine).unwrap()) | |||||
| .wrap_err("failed to send start dataflow message")?; | |||||
| let result: ControlRequestReply = | |||||
| serde_json::from_slice(&reply_raw).wrap_err("failed to parse reply")?; | |||||
| match result { | |||||
| ControlRequestReply::CliAndDefaultDaemonIps { | |||||
| default_daemon, | |||||
| cli, | |||||
| } => Ok(default_daemon.is_some() && default_daemon == cli), | |||||
| ControlRequestReply::Error(err) => bail!("{err}"), | |||||
| other => bail!("unexpected start dataflow reply: {other:?}"), | |||||
| } | |||||
| } | |||||
| @@ -1,42 +1,19 @@ | |||||
| use colored::Colorize; | use colored::Colorize; | ||||
| use communication_layer_request_reply::{RequestReplyLayer, TcpLayer, TcpRequestReplyConnection}; | |||||
| use dora_coordinator::Event; | |||||
| use dora_core::{ | |||||
| descriptor::{source_is_url, Descriptor, DescriptorExt}, | |||||
| topics::{ | |||||
| DORA_COORDINATOR_PORT_CONTROL_DEFAULT, DORA_COORDINATOR_PORT_DEFAULT, | |||||
| DORA_DAEMON_LOCAL_LISTEN_PORT_DEFAULT, | |||||
| }, | |||||
| }; | |||||
| use dora_daemon::{Daemon, LogDestination}; | |||||
| use dora_download::download_file; | |||||
| use dora_message::{ | |||||
| cli_to_coordinator::ControlRequest, | |||||
| coordinator_to_cli::{ControlRequestReply, DataflowList, DataflowResult, DataflowStatus}, | |||||
| }; | |||||
| #[cfg(feature = "tracing")] | |||||
| use dora_tracing::TracingBuilder; | |||||
| use duration_str::parse; | |||||
| use eyre::{bail, Context}; | |||||
| use formatting::FormatDataflowError; | |||||
| use std::{env::current_dir, io::Write, net::SocketAddr}; | |||||
| use command::Executable; | |||||
| use std::{ | use std::{ | ||||
| net::{IpAddr, Ipv4Addr}, | net::{IpAddr, Ipv4Addr}, | ||||
| path::PathBuf, | path::PathBuf, | ||||
| time::Duration, | |||||
| }; | }; | ||||
| use tabwriter::TabWriter; | |||||
| use tokio::runtime::Builder; | |||||
| use tracing::level_filters::LevelFilter; | |||||
| use uuid::Uuid; | |||||
| pub mod command; | |||||
| mod command; | |||||
| mod common; | |||||
| mod formatting; | mod formatting; | ||||
| mod graph; | |||||
| pub mod output; | pub mod output; | ||||
| pub mod session; | pub mod session; | ||||
| mod template; | mod template; | ||||
| pub use command::run_func; | |||||
| const LOCALHOST: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); | const LOCALHOST: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); | ||||
| const LISTEN_WILDCARD: IpAddr = IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)); | const LISTEN_WILDCARD: IpAddr = IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)); | ||||
| @@ -44,228 +21,7 @@ const LISTEN_WILDCARD: IpAddr = IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)); | |||||
| #[clap(version)] | #[clap(version)] | ||||
| pub struct Args { | pub struct Args { | ||||
| #[clap(subcommand)] | #[clap(subcommand)] | ||||
| command: Command, | |||||
| } | |||||
| /// dora-rs cli client | |||||
| #[derive(Debug, clap::Subcommand)] | |||||
| enum Command { | |||||
| /// Check if the coordinator and the daemon is running. | |||||
| Check { | |||||
| /// Path to the dataflow descriptor file (enables additional checks) | |||||
| #[clap(long, value_name = "PATH", value_hint = clap::ValueHint::FilePath)] | |||||
| dataflow: Option<PathBuf>, | |||||
| /// Address of the dora coordinator | |||||
| #[clap(long, value_name = "IP", default_value_t = LOCALHOST)] | |||||
| coordinator_addr: IpAddr, | |||||
| /// Port number of the coordinator control server | |||||
| #[clap(long, value_name = "PORT", default_value_t = DORA_COORDINATOR_PORT_CONTROL_DEFAULT)] | |||||
| coordinator_port: u16, | |||||
| }, | |||||
| /// Generate a visualization of the given graph using mermaid.js. Use --open to open browser. | |||||
| Graph { | |||||
| /// Path to the dataflow descriptor file | |||||
| #[clap(value_name = "PATH", value_hint = clap::ValueHint::FilePath)] | |||||
| dataflow: PathBuf, | |||||
| /// Visualize the dataflow as a Mermaid diagram (instead of HTML) | |||||
| #[clap(long, action)] | |||||
| mermaid: bool, | |||||
| /// Open the HTML visualization in the browser | |||||
| #[clap(long, action)] | |||||
| open: bool, | |||||
| }, | |||||
| /// Run build commands provided in the given dataflow. | |||||
| Build { | |||||
| /// Path to the dataflow descriptor file | |||||
| #[clap(value_name = "PATH")] | |||||
| dataflow: String, | |||||
| /// Address of the dora coordinator | |||||
| #[clap(long, value_name = "IP")] | |||||
| coordinator_addr: Option<IpAddr>, | |||||
| /// Port number of the coordinator control server | |||||
| #[clap(long, value_name = "PORT")] | |||||
| coordinator_port: Option<u16>, | |||||
| // Use UV to build nodes. | |||||
| #[clap(long, action)] | |||||
| uv: bool, | |||||
| // Run build on local machine | |||||
| #[clap(long, action)] | |||||
| local: bool, | |||||
| }, | |||||
| /// Generate a new project or node. Choose the language between Rust, Python, C or C++. | |||||
| New { | |||||
| #[clap(flatten)] | |||||
| args: CommandNew, | |||||
| #[clap(hide = true, long)] | |||||
| internal_create_with_path_dependencies: bool, | |||||
| }, | |||||
| /// Run a dataflow locally. | |||||
| /// | |||||
| /// Directly runs the given dataflow without connecting to a dora | |||||
| /// coordinator or daemon. The dataflow is executed on the local machine. | |||||
| Run { | |||||
| /// Path to the dataflow descriptor file | |||||
| #[clap(value_name = "PATH")] | |||||
| dataflow: String, | |||||
| // Use UV to run nodes. | |||||
| #[clap(long, action)] | |||||
| uv: bool, | |||||
| }, | |||||
| /// Spawn coordinator and daemon in local mode (with default config) | |||||
| Up { | |||||
| /// Use a custom configuration | |||||
| #[clap(long, hide = true, value_name = "PATH", value_hint = clap::ValueHint::FilePath)] | |||||
| config: Option<PathBuf>, | |||||
| }, | |||||
| /// Destroy running coordinator and daemon. If some dataflows are still running, they will be stopped first. | |||||
| Destroy { | |||||
| /// Use a custom configuration | |||||
| #[clap(long, hide = true)] | |||||
| config: Option<PathBuf>, | |||||
| /// Address of the dora coordinator | |||||
| #[clap(long, value_name = "IP", default_value_t = LOCALHOST)] | |||||
| coordinator_addr: IpAddr, | |||||
| /// Port number of the coordinator control server | |||||
| #[clap(long, value_name = "PORT", default_value_t = DORA_COORDINATOR_PORT_CONTROL_DEFAULT)] | |||||
| coordinator_port: u16, | |||||
| }, | |||||
| /// Start the given dataflow path. Attach a name to the running dataflow by using --name. | |||||
| Start { | |||||
| /// Path to the dataflow descriptor file | |||||
| #[clap(value_name = "PATH")] | |||||
| dataflow: String, | |||||
| /// Assign a name to the dataflow | |||||
| #[clap(long)] | |||||
| name: Option<String>, | |||||
| /// Address of the dora coordinator | |||||
| #[clap(long, value_name = "IP", default_value_t = LOCALHOST)] | |||||
| coordinator_addr: IpAddr, | |||||
| /// Port number of the coordinator control server | |||||
| #[clap(long, value_name = "PORT", default_value_t = DORA_COORDINATOR_PORT_CONTROL_DEFAULT)] | |||||
| coordinator_port: u16, | |||||
| /// Attach to the dataflow and wait for its completion | |||||
| #[clap(long, action)] | |||||
| attach: bool, | |||||
| /// Run the dataflow in background | |||||
| #[clap(long, action)] | |||||
| detach: bool, | |||||
| /// Enable hot reloading (Python only) | |||||
| #[clap(long, action)] | |||||
| hot_reload: bool, | |||||
| // Use UV to run nodes. | |||||
| #[clap(long, action)] | |||||
| uv: bool, | |||||
| }, | |||||
| /// Stop the given dataflow UUID. If no id is provided, you will be able to choose between the running dataflows. | |||||
| Stop { | |||||
| /// UUID of the dataflow that should be stopped | |||||
| uuid: Option<Uuid>, | |||||
| /// Name of the dataflow that should be stopped | |||||
| #[clap(long)] | |||||
| name: Option<String>, | |||||
| /// Kill the dataflow if it doesn't stop after the given duration | |||||
| #[clap(long, value_name = "DURATION")] | |||||
| #[arg(value_parser = parse)] | |||||
| grace_duration: Option<Duration>, | |||||
| /// Address of the dora coordinator | |||||
| #[clap(long, value_name = "IP", default_value_t = LOCALHOST)] | |||||
| coordinator_addr: IpAddr, | |||||
| /// Port number of the coordinator control server | |||||
| #[clap(long, value_name = "PORT", default_value_t = DORA_COORDINATOR_PORT_CONTROL_DEFAULT)] | |||||
| coordinator_port: u16, | |||||
| }, | |||||
| /// List running dataflows. | |||||
| List { | |||||
| /// Address of the dora coordinator | |||||
| #[clap(long, value_name = "IP", default_value_t = LOCALHOST)] | |||||
| coordinator_addr: IpAddr, | |||||
| /// Port number of the coordinator control server | |||||
| #[clap(long, value_name = "PORT", default_value_t = DORA_COORDINATOR_PORT_CONTROL_DEFAULT)] | |||||
| coordinator_port: u16, | |||||
| }, | |||||
| // Planned for future releases: | |||||
| // Dashboard, | |||||
| /// Show logs of a given dataflow and node. | |||||
| #[command(allow_missing_positional = true)] | |||||
| Logs { | |||||
| /// Identifier of the dataflow | |||||
| #[clap(value_name = "UUID_OR_NAME")] | |||||
| dataflow: Option<String>, | |||||
| /// Show logs for the given node | |||||
| #[clap(value_name = "NAME")] | |||||
| node: String, | |||||
| /// Address of the dora coordinator | |||||
| #[clap(long, value_name = "IP", default_value_t = LOCALHOST)] | |||||
| coordinator_addr: IpAddr, | |||||
| /// Port number of the coordinator control server | |||||
| #[clap(long, value_name = "PORT", default_value_t = DORA_COORDINATOR_PORT_CONTROL_DEFAULT)] | |||||
| coordinator_port: u16, | |||||
| }, | |||||
| // Metrics, | |||||
| // Stats, | |||||
| // Get, | |||||
| // Upgrade, | |||||
| /// Run daemon | |||||
| Daemon { | |||||
| /// Unique identifier for the machine (required for distributed dataflows) | |||||
| #[clap(long)] | |||||
| machine_id: Option<String>, | |||||
| /// Local listen port for event such as dynamic node. | |||||
| #[clap(long, default_value_t = DORA_DAEMON_LOCAL_LISTEN_PORT_DEFAULT)] | |||||
| local_listen_port: u16, | |||||
| /// Address and port number of the dora coordinator | |||||
| #[clap(long, short, default_value_t = LOCALHOST)] | |||||
| coordinator_addr: IpAddr, | |||||
| /// Port number of the coordinator control server | |||||
| #[clap(long, default_value_t = DORA_COORDINATOR_PORT_DEFAULT)] | |||||
| coordinator_port: u16, | |||||
| #[clap(long, hide = true)] | |||||
| run_dataflow: Option<PathBuf>, | |||||
| /// Suppresses all log output to stdout. | |||||
| #[clap(long)] | |||||
| quiet: bool, | |||||
| }, | |||||
| /// Run runtime | |||||
| Runtime, | |||||
| /// Run coordinator | |||||
| Coordinator { | |||||
| /// Network interface to bind to for daemon communication | |||||
| #[clap(long, default_value_t = LISTEN_WILDCARD)] | |||||
| interface: IpAddr, | |||||
| /// Port number to bind to for daemon communication | |||||
| #[clap(long, default_value_t = DORA_COORDINATOR_PORT_DEFAULT)] | |||||
| port: u16, | |||||
| /// Network interface to bind to for control communication | |||||
| #[clap(long, default_value_t = LISTEN_WILDCARD)] | |||||
| control_interface: IpAddr, | |||||
| /// Port number to bind to for control communication | |||||
| #[clap(long, default_value_t = DORA_COORDINATOR_PORT_CONTROL_DEFAULT)] | |||||
| control_port: u16, | |||||
| /// Suppresses all log output to stdout. | |||||
| #[clap(long)] | |||||
| quiet: bool, | |||||
| }, | |||||
| /// Dora CLI self-management commands | |||||
| Self_ { | |||||
| #[clap(subcommand)] | |||||
| command: SelfSubCommand, | |||||
| }, | |||||
| } | |||||
| #[derive(Debug, clap::Subcommand)] | |||||
| enum SelfSubCommand { | |||||
| /// Check for updates or update the CLI | |||||
| Update { | |||||
| /// Only check for updates without installing | |||||
| #[clap(long)] | |||||
| check_only: bool, | |||||
| }, | |||||
| /// Remove The Dora CLI from the system | |||||
| Uninstall { | |||||
| /// Force uninstallation without confirmation | |||||
| #[clap(long)] | |||||
| force: bool, | |||||
| }, | |||||
| command: command::Command, | |||||
| } | } | ||||
| #[derive(Debug, clap::Args)] | #[derive(Debug, clap::Args)] | ||||
| @@ -298,503 +54,13 @@ enum Lang { | |||||
| } | } | ||||
| pub fn lib_main(args: Args) { | pub fn lib_main(args: Args) { | ||||
| if let Err(err) = run_cli(args) { | |||||
| if let Err(err) = args.command.execute() { | |||||
| eprintln!("\n\n{}", "[ERROR]".bold().red()); | eprintln!("\n\n{}", "[ERROR]".bold().red()); | ||||
| eprintln!("{err:?}"); | eprintln!("{err:?}"); | ||||
| std::process::exit(1); | std::process::exit(1); | ||||
| } | } | ||||
| } | } | ||||
| fn run_cli(args: Args) -> eyre::Result<()> { | |||||
| tracing_log::LogTracer::init()?; | |||||
| #[cfg(feature = "tracing")] | |||||
| match &args.command { | |||||
| Command::Daemon { | |||||
| quiet, machine_id, .. | |||||
| } => { | |||||
| let name = "dora-daemon"; | |||||
| let filename = machine_id | |||||
| .as_ref() | |||||
| .map(|id| format!("{name}-{id}")) | |||||
| .unwrap_or(name.to_string()); | |||||
| let mut builder = TracingBuilder::new(name); | |||||
| if !quiet { | |||||
| builder = builder.with_stdout("info,zenoh=warn"); | |||||
| } | |||||
| builder = builder.with_file(filename, LevelFilter::INFO)?; | |||||
| builder | |||||
| .build() | |||||
| .wrap_err("failed to set up tracing subscriber")?; | |||||
| } | |||||
| Command::Runtime => { | |||||
| // Do not set the runtime in the cli. | |||||
| } | |||||
| Command::Coordinator { quiet, .. } => { | |||||
| let name = "dora-coordinator"; | |||||
| let mut builder = TracingBuilder::new(name); | |||||
| if !quiet { | |||||
| builder = builder.with_stdout("info"); | |||||
| } | |||||
| builder = builder.with_file(name, LevelFilter::INFO)?; | |||||
| builder | |||||
| .build() | |||||
| .wrap_err("failed to set up tracing subscriber")?; | |||||
| } | |||||
| Command::Run { .. } | Command::Build { .. } => { | |||||
| let log_level = std::env::var("RUST_LOG").ok().unwrap_or("info".to_string()); | |||||
| TracingBuilder::new("run") | |||||
| .with_stdout(log_level) | |||||
| .build() | |||||
| .wrap_err("failed to set up tracing subscriber")?; | |||||
| } | |||||
| _ => { | |||||
| TracingBuilder::new("dora-cli") | |||||
| .with_stdout("warn") | |||||
| .build() | |||||
| .wrap_err("failed to set up tracing subscriber")?; | |||||
| } | |||||
| }; | |||||
| match args.command { | |||||
| Command::Check { | |||||
| dataflow, | |||||
| coordinator_addr, | |||||
| coordinator_port, | |||||
| } => match dataflow { | |||||
| Some(dataflow) => { | |||||
| let working_dir = dataflow | |||||
| .canonicalize() | |||||
| .context("failed to canonicalize dataflow path")? | |||||
| .parent() | |||||
| .ok_or_else(|| eyre::eyre!("dataflow path has no parent dir"))? | |||||
| .to_owned(); | |||||
| Descriptor::blocking_read(&dataflow)?.check(&working_dir)?; | |||||
| command::check::check_environment((coordinator_addr, coordinator_port).into())? | |||||
| } | |||||
| None => command::check::check_environment((coordinator_addr, coordinator_port).into())?, | |||||
| }, | |||||
| Command::Graph { | |||||
| dataflow, | |||||
| mermaid, | |||||
| open, | |||||
| } => { | |||||
| graph::create(dataflow, mermaid, open)?; | |||||
| } | |||||
| Command::Build { | |||||
| dataflow, | |||||
| coordinator_addr, | |||||
| coordinator_port, | |||||
| uv, | |||||
| local, | |||||
| } => command::build(dataflow, coordinator_addr, coordinator_port, uv, local)?, | |||||
| Command::New { | |||||
| args, | |||||
| internal_create_with_path_dependencies, | |||||
| } => template::create(args, internal_create_with_path_dependencies)?, | |||||
| Command::Run { dataflow, uv } => command::run(dataflow, uv)?, | |||||
| Command::Up { config } => { | |||||
| command::up::up(config.as_deref())?; | |||||
| } | |||||
| Command::Logs { | |||||
| dataflow, | |||||
| node, | |||||
| coordinator_addr, | |||||
| coordinator_port, | |||||
| } => { | |||||
| let mut session = connect_to_coordinator((coordinator_addr, coordinator_port).into()) | |||||
| .wrap_err("failed to connect to dora coordinator")?; | |||||
| let list = query_running_dataflows(&mut *session) | |||||
| .wrap_err("failed to query running dataflows")?; | |||||
| if let Some(dataflow) = dataflow { | |||||
| let uuid = Uuid::parse_str(&dataflow).ok(); | |||||
| let name = if uuid.is_some() { None } else { Some(dataflow) }; | |||||
| command::logs(&mut *session, uuid, name, node)? | |||||
| } else { | |||||
| let active: Vec<dora_message::coordinator_to_cli::DataflowIdAndName> = | |||||
| list.get_active(); | |||||
| let uuid = match &active[..] { | |||||
| [] => bail!("No dataflows are running"), | |||||
| [uuid] => uuid.clone(), | |||||
| _ => inquire::Select::new("Choose dataflow to show logs:", active).prompt()?, | |||||
| }; | |||||
| command::logs(&mut *session, Some(uuid.uuid), None, node)? | |||||
| } | |||||
| } | |||||
| Command::Start { | |||||
| dataflow, | |||||
| name, | |||||
| coordinator_addr, | |||||
| coordinator_port, | |||||
| attach, | |||||
| detach, | |||||
| hot_reload, | |||||
| uv, | |||||
| } => { | |||||
| let coordinator_socket = (coordinator_addr, coordinator_port).into(); | |||||
| command::start( | |||||
| dataflow, | |||||
| name, | |||||
| coordinator_socket, | |||||
| attach, | |||||
| detach, | |||||
| hot_reload, | |||||
| uv, | |||||
| )? | |||||
| } | |||||
| Command::List { | |||||
| coordinator_addr, | |||||
| coordinator_port, | |||||
| } => match connect_to_coordinator((coordinator_addr, coordinator_port).into()) { | |||||
| Ok(mut session) => list(&mut *session)?, | |||||
| Err(_) => { | |||||
| bail!("No dora coordinator seems to be running."); | |||||
| } | |||||
| }, | |||||
| Command::Stop { | |||||
| uuid, | |||||
| name, | |||||
| grace_duration, | |||||
| coordinator_addr, | |||||
| coordinator_port, | |||||
| } => { | |||||
| let mut session = connect_to_coordinator((coordinator_addr, coordinator_port).into()) | |||||
| .wrap_err("could not connect to dora coordinator")?; | |||||
| match (uuid, name) { | |||||
| (Some(uuid), _) => stop_dataflow(uuid, grace_duration, &mut *session)?, | |||||
| (None, Some(name)) => stop_dataflow_by_name(name, grace_duration, &mut *session)?, | |||||
| (None, None) => stop_dataflow_interactive(grace_duration, &mut *session)?, | |||||
| } | |||||
| } | |||||
| Command::Destroy { | |||||
| config, | |||||
| coordinator_addr, | |||||
| coordinator_port, | |||||
| } => command::up::destroy( | |||||
| config.as_deref(), | |||||
| (coordinator_addr, coordinator_port).into(), | |||||
| )?, | |||||
| Command::Coordinator { | |||||
| interface, | |||||
| port, | |||||
| control_interface, | |||||
| control_port, | |||||
| quiet, | |||||
| } => { | |||||
| let rt = Builder::new_multi_thread() | |||||
| .enable_all() | |||||
| .build() | |||||
| .context("tokio runtime failed")?; | |||||
| rt.block_on(async { | |||||
| let bind = SocketAddr::new(interface, port); | |||||
| let bind_control = SocketAddr::new(control_interface, control_port); | |||||
| let (port, task) = | |||||
| dora_coordinator::start(bind, bind_control, futures::stream::empty::<Event>()) | |||||
| .await?; | |||||
| if !quiet { | |||||
| println!("Listening for incoming daemon connection on {port}"); | |||||
| } | |||||
| task.await | |||||
| }) | |||||
| .context("failed to run dora-coordinator")? | |||||
| } | |||||
| Command::Daemon { | |||||
| coordinator_addr, | |||||
| coordinator_port, | |||||
| local_listen_port, | |||||
| machine_id, | |||||
| run_dataflow, | |||||
| quiet: _, | |||||
| } => { | |||||
| let rt = Builder::new_multi_thread() | |||||
| .enable_all() | |||||
| .build() | |||||
| .context("tokio runtime failed")?; | |||||
| rt.block_on(async { | |||||
| match run_dataflow { | |||||
| Some(dataflow_path) => { | |||||
| tracing::info!("Starting dataflow `{}`", dataflow_path.display()); | |||||
| if coordinator_addr != LOCALHOST { | |||||
| tracing::info!( | |||||
| "Not using coordinator addr {} as `run_dataflow` is for local dataflow only. Please use the `start` command for remote coordinator", | |||||
| coordinator_addr | |||||
| ); | |||||
| } | |||||
| let dataflow_session = | |||||
| DataflowSession::read_session(&dataflow_path).context("failed to read DataflowSession")?; | |||||
| let result = Daemon::run_dataflow(&dataflow_path, | |||||
| dataflow_session.build_id, dataflow_session.local_build, dataflow_session.session_id, false, | |||||
| LogDestination::Tracing, | |||||
| ).await?; | |||||
| handle_dataflow_result(result, None) | |||||
| } | |||||
| None => { | |||||
| Daemon::run(SocketAddr::new(coordinator_addr, coordinator_port), machine_id, local_listen_port).await | |||||
| } | |||||
| } | |||||
| }) | |||||
| .context("failed to run dora-daemon")? | |||||
| } | |||||
| Command::Runtime => dora_runtime::main().context("Failed to run dora-runtime")?, | |||||
| Command::Self_ { command } => match command { | |||||
| SelfSubCommand::Update { check_only } => { | |||||
| println!("Checking for updates..."); | |||||
| #[cfg(target_os = "linux")] | |||||
| let bin_path_in_archive = format!("dora-cli-{}/dora", env!("TARGET")); | |||||
| #[cfg(target_os = "macos")] | |||||
| let bin_path_in_archive = format!("dora-cli-{}/dora", env!("TARGET")); | |||||
| #[cfg(target_os = "windows")] | |||||
| let bin_path_in_archive = String::from("dora.exe"); | |||||
| let status = self_update::backends::github::Update::configure() | |||||
| .repo_owner("dora-rs") | |||||
| .repo_name("dora") | |||||
| .bin_path_in_archive(&bin_path_in_archive) | |||||
| .bin_name("dora") | |||||
| .show_download_progress(true) | |||||
| .current_version(env!("CARGO_PKG_VERSION")) | |||||
| .build()?; | |||||
| if check_only { | |||||
| // Only check if an update is available | |||||
| match status.get_latest_release() { | |||||
| Ok(release) => { | |||||
| let current_version = self_update::cargo_crate_version!(); | |||||
| if current_version != release.version { | |||||
| println!( | |||||
| "An update is available: {}. Run 'dora self update' to update", | |||||
| release.version | |||||
| ); | |||||
| } else { | |||||
| println!( | |||||
| "Dora CLI is already at the latest version: {}", | |||||
| current_version | |||||
| ); | |||||
| } | |||||
| } | |||||
| Err(e) => println!("Failed to check for updates: {}", e), | |||||
| } | |||||
| } else { | |||||
| // Perform the actual update | |||||
| match status.update() { | |||||
| Ok(update_status) => match update_status { | |||||
| self_update::Status::UpToDate(version) => { | |||||
| println!("Dora CLI is already at the latest version: {}", version); | |||||
| } | |||||
| self_update::Status::Updated(version) => { | |||||
| println!("Successfully updated Dora CLI to version: {}", version); | |||||
| } | |||||
| }, | |||||
| Err(e) => println!("Failed to update: {}", e), | |||||
| } | |||||
| } | |||||
| } | |||||
| SelfSubCommand::Uninstall { force } => { | |||||
| if !force { | |||||
| let confirmed = | |||||
| inquire::Confirm::new("Are you sure you want to uninstall Dora CLI?") | |||||
| .with_default(false) | |||||
| .prompt() | |||||
| .wrap_err("Uninstallation cancelled")?; | |||||
| if !confirmed { | |||||
| println!("Uninstallation cancelled"); | |||||
| return Ok(()); | |||||
| } | |||||
| } | |||||
| println!("Uninstalling Dora CLI..."); | |||||
| #[cfg(feature = "python")] | |||||
| { | |||||
| println!("Detected Python installation..."); | |||||
| // Try uv pip uninstall first | |||||
| let uv_status = std::process::Command::new("uv") | |||||
| .args(["pip", "uninstall", "dora-rs-cli"]) | |||||
| .status(); | |||||
| if let Ok(status) = uv_status { | |||||
| if status.success() { | |||||
| println!("Dora CLI has been successfully uninstalled via uv pip."); | |||||
| return Ok(()); | |||||
| } | |||||
| } | |||||
| // Fall back to regular pip uninstall | |||||
| println!("Trying with pip..."); | |||||
| let status = std::process::Command::new("pip") | |||||
| .args(["uninstall", "-y", "dora-rs-cli"]) | |||||
| .status() | |||||
| .wrap_err("Failed to run pip uninstall")?; | |||||
| if status.success() { | |||||
| println!("Dora CLI has been successfully uninstalled via pip."); | |||||
| } else { | |||||
| bail!("Failed to uninstall Dora CLI via pip."); | |||||
| } | |||||
| } | |||||
| #[cfg(not(feature = "python"))] | |||||
| { | |||||
| match self_replace::self_delete() { | |||||
| Ok(_) => { | |||||
| println!("Dora CLI has been successfully uninstalled."); | |||||
| } | |||||
| Err(e) => { | |||||
| bail!("Failed to uninstall Dora CLI: {}", e); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| }, | |||||
| }; | |||||
| Ok(()) | |||||
| } | |||||
| fn stop_dataflow_interactive( | |||||
| grace_duration: Option<Duration>, | |||||
| session: &mut TcpRequestReplyConnection, | |||||
| ) -> eyre::Result<()> { | |||||
| let list = query_running_dataflows(session).wrap_err("failed to query running dataflows")?; | |||||
| let active = list.get_active(); | |||||
| if active.is_empty() { | |||||
| eprintln!("No dataflows are running"); | |||||
| } else { | |||||
| let selection = inquire::Select::new("Choose dataflow to stop:", active).prompt()?; | |||||
| stop_dataflow(selection.uuid, grace_duration, session)?; | |||||
| } | |||||
| Ok(()) | |||||
| } | |||||
| fn stop_dataflow( | |||||
| uuid: Uuid, | |||||
| grace_duration: Option<Duration>, | |||||
| session: &mut TcpRequestReplyConnection, | |||||
| ) -> Result<(), eyre::ErrReport> { | |||||
| let reply_raw = session | |||||
| .request( | |||||
| &serde_json::to_vec(&ControlRequest::Stop { | |||||
| dataflow_uuid: uuid, | |||||
| grace_duration, | |||||
| }) | |||||
| .unwrap(), | |||||
| ) | |||||
| .wrap_err("failed to send dataflow stop message")?; | |||||
| let result: ControlRequestReply = | |||||
| serde_json::from_slice(&reply_raw).wrap_err("failed to parse reply")?; | |||||
| match result { | |||||
| ControlRequestReply::DataflowStopped { uuid, result } => { | |||||
| handle_dataflow_result(result, Some(uuid)) | |||||
| } | |||||
| ControlRequestReply::Error(err) => bail!("{err}"), | |||||
| other => bail!("unexpected stop dataflow reply: {other:?}"), | |||||
| } | |||||
| } | |||||
| fn handle_dataflow_result(result: DataflowResult, uuid: Option<Uuid>) -> Result<(), eyre::Error> { | |||||
| if result.is_ok() { | |||||
| Ok(()) | |||||
| } else { | |||||
| Err(match uuid { | |||||
| Some(uuid) => { | |||||
| eyre::eyre!("Dataflow {uuid} failed:\n{}", FormatDataflowError(&result)) | |||||
| } | |||||
| None => { | |||||
| eyre::eyre!("Dataflow failed:\n{}", FormatDataflowError(&result)) | |||||
| } | |||||
| }) | |||||
| } | |||||
| } | |||||
| fn stop_dataflow_by_name( | |||||
| name: String, | |||||
| grace_duration: Option<Duration>, | |||||
| session: &mut TcpRequestReplyConnection, | |||||
| ) -> Result<(), eyre::ErrReport> { | |||||
| let reply_raw = session | |||||
| .request( | |||||
| &serde_json::to_vec(&ControlRequest::StopByName { | |||||
| name, | |||||
| grace_duration, | |||||
| }) | |||||
| .unwrap(), | |||||
| ) | |||||
| .wrap_err("failed to send dataflow stop_by_name message")?; | |||||
| let result: ControlRequestReply = | |||||
| serde_json::from_slice(&reply_raw).wrap_err("failed to parse reply")?; | |||||
| match result { | |||||
| ControlRequestReply::DataflowStopped { uuid, result } => { | |||||
| handle_dataflow_result(result, Some(uuid)) | |||||
| } | |||||
| ControlRequestReply::Error(err) => bail!("{err}"), | |||||
| other => bail!("unexpected stop dataflow reply: {other:?}"), | |||||
| } | |||||
| } | |||||
| fn list(session: &mut TcpRequestReplyConnection) -> Result<(), eyre::ErrReport> { | |||||
| let list = query_running_dataflows(session)?; | |||||
| let mut tw = TabWriter::new(vec![]); | |||||
| tw.write_all(b"UUID\tName\tStatus\n")?; | |||||
| for entry in list.0 { | |||||
| let uuid = entry.id.uuid; | |||||
| let name = entry.id.name.unwrap_or_default(); | |||||
| let status = match entry.status { | |||||
| DataflowStatus::Running => "Running", | |||||
| DataflowStatus::Finished => "Succeeded", | |||||
| DataflowStatus::Failed => "Failed", | |||||
| }; | |||||
| tw.write_all(format!("{uuid}\t{name}\t{status}\n").as_bytes())?; | |||||
| } | |||||
| tw.flush()?; | |||||
| let formatted = String::from_utf8(tw.into_inner()?)?; | |||||
| println!("{formatted}"); | |||||
| Ok(()) | |||||
| } | |||||
| fn query_running_dataflows(session: &mut TcpRequestReplyConnection) -> eyre::Result<DataflowList> { | |||||
| let reply_raw = session | |||||
| .request(&serde_json::to_vec(&ControlRequest::List).unwrap()) | |||||
| .wrap_err("failed to send list message")?; | |||||
| let reply: ControlRequestReply = | |||||
| serde_json::from_slice(&reply_raw).wrap_err("failed to parse reply")?; | |||||
| let ids = match reply { | |||||
| ControlRequestReply::DataflowList(list) => list, | |||||
| ControlRequestReply::Error(err) => bail!("{err}"), | |||||
| other => bail!("unexpected list dataflow reply: {other:?}"), | |||||
| }; | |||||
| Ok(ids) | |||||
| } | |||||
| fn connect_to_coordinator( | |||||
| coordinator_addr: SocketAddr, | |||||
| ) -> std::io::Result<Box<TcpRequestReplyConnection>> { | |||||
| TcpLayer::new().connect(coordinator_addr) | |||||
| } | |||||
| fn resolve_dataflow(dataflow: String) -> eyre::Result<PathBuf> { | |||||
| let dataflow = if source_is_url(&dataflow) { | |||||
| // try to download the shared library | |||||
| let target_path = current_dir().context("Could not access the current dir")?; | |||||
| let rt = Builder::new_current_thread() | |||||
| .enable_all() | |||||
| .build() | |||||
| .context("tokio runtime failed")?; | |||||
| rt.block_on(async { download_file(&dataflow, &target_path).await }) | |||||
| .wrap_err("failed to download dataflow yaml file")? | |||||
| } else { | |||||
| PathBuf::from(dataflow) | |||||
| }; | |||||
| Ok(dataflow) | |||||
| } | |||||
| #[cfg(feature = "python")] | #[cfg(feature = "python")] | ||||
| use clap::Parser; | use clap::Parser; | ||||
| #[cfg(feature = "python")] | #[cfg(feature = "python")] | ||||
| @@ -804,8 +70,6 @@ use pyo3::{ | |||||
| wrap_pyfunction, Bound, PyResult, Python, | wrap_pyfunction, Bound, PyResult, Python, | ||||
| }; | }; | ||||
| use crate::session::DataflowSession; | |||||
| #[cfg(feature = "python")] | #[cfg(feature = "python")] | ||||
| #[pyfunction] | #[pyfunction] | ||||
| fn py_main(_py: Python) -> PyResult<()> { | fn py_main(_py: Python) -> PyResult<()> { | ||||
| @@ -19,7 +19,7 @@ futures = "0.3.21" | |||||
| tokio = { version = "1.24.2", features = ["full"] } | tokio = { version = "1.24.2", features = ["full"] } | ||||
| tokio-stream = { version = "0.1.8", features = ["io-util", "net"] } | tokio-stream = { version = "0.1.8", features = ["io-util", "net"] } | ||||
| uuid = { version = "1.2.1" } | uuid = { version = "1.2.1" } | ||||
| dora-core = { workspace = true } | |||||
| dora-core = { workspace = true, features = ["build"] } | |||||
| tracing = "0.1.36" | tracing = "0.1.36" | ||||
| dora-tracing = { workspace = true, optional = true } | dora-tracing = { workspace = true, optional = true } | ||||
| futures-concurrency = "7.1.0" | futures-concurrency = "7.1.0" | ||||
| @@ -430,19 +430,6 @@ impl PreparedNode { | |||||
| } | } | ||||
| }; | }; | ||||
| if buffer.contains("TRACE") | |||||
| || buffer.contains("INFO") | |||||
| || buffer.contains("DEBUG") | |||||
| || buffer.contains("WARN") | |||||
| || buffer.contains("ERROR") | |||||
| { | |||||
| // tracing output, potentially multi-line -> keep reading following lines | |||||
| // until double-newline | |||||
| if !buffer.ends_with("\n\n") && !finished { | |||||
| continue; | |||||
| } | |||||
| } | |||||
| // send the buffered lines | // send the buffered lines | ||||
| let lines = std::mem::take(&mut buffer); | let lines = std::mem::take(&mut buffer); | ||||
| let sent = stdout_tx.send(lines.clone()).await; | let sent = stdout_tx.send(lines.clone()).await; | ||||
| @@ -73,7 +73,7 @@ int main() | |||||
| std::cerr << "Unknown event type " << static_cast<int>(ty) << std::endl; | std::cerr << "Unknown event type " << static_cast<int>(ty) << std::endl; | ||||
| } | } | ||||
| if (received_ticks > 20) | |||||
| if (received_ticks > 20 && responses_received > 0) | |||||
| { | { | ||||
| break; | break; | ||||
| } | } | ||||
| @@ -0,0 +1,60 @@ | |||||
| ## SO101 Arm Control | |||||
| This example provides gamepad control and leader-follower functionality for the SO-101 robotic arm. | |||||
| ### Install Dependencies | |||||
| install the required Python packages for rerun visualization (optional): | |||||
| ```bash | |||||
| # Install the URDF loader for Rerun visualization | |||||
| pip install git+https://github.com/dora-rs/rerun-loader-python-urdf | |||||
| ``` | |||||
| ### Hardware Setup | |||||
| 1. Connect your SO-101 arm(s) to your computer via USB/serial | |||||
| 2. Note the serial port names (e.g.,for linux `/dev/ttyACM0`, `/dev/ttyACM1`) | |||||
| 3. Connect your gamepad controller | |||||
| 4. Update the `PORT` environment variable in the YAML files | |||||
| #### Single Arm Control (arm_gamepad_control.yml) | |||||
| Control a single SO-101 arm with gamepad input and visualization: | |||||
| ```bash | |||||
| dora build arm.yml | |||||
| dora run arm.yml | |||||
| ``` | |||||
| #### Leader-Follower Mode (leader_follower.yml) | |||||
| Use one arm as a leader to control another follower arm: | |||||
| ```bash | |||||
| dora build leader.yml | |||||
| dora run leader.yml | |||||
| ``` | |||||
| #### Serial Port Configuration | |||||
| Update the `PORT` environment variable in the YAML files: | |||||
| ```yaml | |||||
| env: | |||||
| PORT: /dev/ttyACM0 # Change to your actual port | |||||
| ``` | |||||
| ## Troubleshooting | |||||
| ### Serial Connection Issues | |||||
| - Check that the arm is powered on and connected | |||||
| - Verify the correct serial port in the YAML configuration | |||||
| - Ensure proper permissions: `sudo chmod +x PORT` | |||||
| ### Gamepad Not Detected | |||||
| - Verify gamepad is connected and recognized by the system | |||||
| - Test with `jstest /dev/input/js0` (Linux) | |||||
| ## Safety Notes | |||||
| - Always ensure the arm has sufficient clearance before operation | |||||
| @@ -0,0 +1,48 @@ | |||||
| nodes: | |||||
| - id: so101 | |||||
| build: pip install -e ../../node-hub/dora-rustypot | |||||
| path: dora-rustypot | |||||
| inputs: | |||||
| tick: dora/timer/millis/10 | |||||
| pose: pytorch_kinematics/cmd_vel | |||||
| outputs: | |||||
| - pose | |||||
| env: | |||||
| PORT: /dev/ttyACM0 | |||||
| IDS: 1 2 3 4 5 | |||||
| - id: pytorch_kinematics | |||||
| build: pip install -e ../../node-hub/dora-pytorch-kinematics | |||||
| path: dora-pytorch-kinematics | |||||
| inputs: | |||||
| cmd_vel: gamepad/cmd_vel | |||||
| outputs: | |||||
| - cmd_vel | |||||
| env: | |||||
| MODEL_NAME: "so_arm101_description" | |||||
| END_EFFECTOR_LINK: "gripper" | |||||
| TRANSFORM: "0. 0. 0. 1. 0. 0. 0." | |||||
| POSITION_TOLERANCE: 0.01 | |||||
| ROTATION_TOLERANCE: 0.03 | |||||
| - id: gamepad | |||||
| build: pip install -e ../../node-hub/gamepad | |||||
| path: gamepad | |||||
| outputs: | |||||
| - cmd_vel | |||||
| - raw_control | |||||
| inputs: | |||||
| tick: dora/timer/millis/10 | |||||
| env: | |||||
| MAX_LINEAR_SPEED: 0.01 | |||||
| MAX_ANGULAR_SPEED: 0.05 | |||||
| # comment below path if you don't want to visualize the arm in rerun | |||||
| - id: plot | |||||
| build: pip install -e ../../node-hub/dora-rerun | |||||
| path: dora-rerun | |||||
| inputs: | |||||
| jointstate_so101_new_calib: so101/pose | |||||
| env: | |||||
| so101_new_calib_urdf: "so_arm101_description" | |||||
| so101_new_calib_transform: "0. 0. 0. 1. 0. 0. 0." | |||||
| @@ -0,0 +1,33 @@ | |||||
| nodes: | |||||
| - id: so101 | |||||
| build: pip install -e ../../node-hub/dora-rustypot | |||||
| path: dora-rustypot | |||||
| inputs: | |||||
| tick: dora/timer/millis/10 | |||||
| pose: leader_interface/pose | |||||
| outputs: | |||||
| - pose | |||||
| env: | |||||
| PORT: /dev/ttyACM0 | |||||
| IDS: 1 2 3 4 5 6 | |||||
| - id: leader_interface | |||||
| build: pip install -e ../../node-hub/dora-rustypot | |||||
| path: dora-rustypot | |||||
| inputs: | |||||
| tick: dora/timer/millis/10 | |||||
| outputs: | |||||
| - pose | |||||
| env: | |||||
| PORT: /dev/ttyACM1 | |||||
| IDS: 1 2 3 4 5 6 | |||||
| # comment below path if you don't want to visualize the arms in rerun | |||||
| - id: plot | |||||
| build: pip install -e ../../node-hub/dora-rerun | |||||
| path: dora-rerun | |||||
| inputs: | |||||
| jointstate_so101_new_calib: so101/pose | |||||
| env: | |||||
| so101_new_calib_urdf: "so_arm101_description" | |||||
| so101_new_calib_transform: "0. 0. 0. 1. 0. 0. 0." | |||||
| @@ -1,4 +1,4 @@ | |||||
| # Dora Speech to Text example | |||||
| # Dora Speech to Speech example | |||||
| Make sure to have, dora, pip and cargo installed. | Make sure to have, dora, pip and cargo installed. | ||||
| @@ -23,6 +23,6 @@ sudo apt-get install espeak | |||||
| ```bash | ```bash | ||||
| uv venv --seed -p 3.11 | uv venv --seed -p 3.11 | ||||
| uv pip install -e ../../apis/python/node --reinstall | uv pip install -e ../../apis/python/node --reinstall | ||||
| dora build kokoro-dev.yml | |||||
| dora run kokoro-dev.yml | |||||
| dora build kokoro-dev.yml --uv | |||||
| dora run kokoro-dev.yml --uv | |||||
| ``` | ``` | ||||
| @@ -0,0 +1,70 @@ | |||||
| nodes: | |||||
| - id: plot | |||||
| build: pip install -e ../../node-hub/dora-rerun | |||||
| path: dora-rerun | |||||
| inputs: | |||||
| jointstate_panda: pytorch_kinematics/cmd_vel | |||||
| camera/image: dora-vggt/image | |||||
| camera/depth: dora-vggt/depth | |||||
| env: | |||||
| panda_urdf: "panda_description" | |||||
| panda_transform: .5 -0. -0.1 1. 0. 0. 0. | |||||
| CAMERA_PITCH: 1.5708 | |||||
| - id: gamepad | |||||
| build: pip install -e ../../node-hub/gamepad | |||||
| path: gamepad | |||||
| outputs: | |||||
| - cmd_vel | |||||
| - raw_control | |||||
| inputs: | |||||
| tick: dora/timer/millis/10 | |||||
| env: | |||||
| MAX_LINEAR_SPEED: 0.01 | |||||
| MAX_ANGULAR_SPEED: 0.05 | |||||
| - id: pytorch_kinematics | |||||
| build: pip install -e ../../node-hub/dora-pytorch-kinematics | |||||
| path: dora-pytorch-kinematics | |||||
| inputs: | |||||
| cmd_vel: gamepad/cmd_vel | |||||
| outputs: | |||||
| - cmd_vel | |||||
| env: | |||||
| MODEL_NAME: "panda_description" | |||||
| END_EFFECTOR_LINK: "panda_link8" | |||||
| TRANSFORM: .5 -0. -0.1 1. 0. 0. 0. | |||||
| POSITION_TOLERANCE: 0.001 | |||||
| ROTATION_TOLERANCE: 0.001 | |||||
| - id: camera | |||||
| build: pip install -e ../../../node-hub/opencv-video-capture | |||||
| path: opencv-video-capture | |||||
| inputs: | |||||
| tick: dora/timer/millis/100 | |||||
| outputs: | |||||
| - image | |||||
| env: | |||||
| CAPTURE_PATH: 4 | |||||
| - id: camera2 | |||||
| build: pip install -e ../../../node-hub/opencv-video-capture | |||||
| path: opencv-video-capture | |||||
| inputs: | |||||
| tick: dora/timer/millis/100 | |||||
| outputs: | |||||
| - image | |||||
| env: | |||||
| CAPTURE_PATH: 6 | |||||
| - id: dora-vggt | |||||
| build: pip install -e ../../../node-hub/dora-vggt | |||||
| path: dora-vggt | |||||
| inputs: | |||||
| image: camera/image | |||||
| image2: camera2/image | |||||
| outputs: | |||||
| - depth | |||||
| - image | |||||
| env: | |||||
| SCALE_FACTOR: 0.9 | |||||
| @@ -0,0 +1,68 @@ | |||||
| nodes: | |||||
| - id: plot | |||||
| build: pip install -e ../../node-hub/dora-rerun | |||||
| path: dora-rerun | |||||
| inputs: | |||||
| jointstate_iiwa14_primitive_collision: pytorch_kinematics/cmd_vel | |||||
| camera/image: dora-vggt/image | |||||
| camera/depth: dora-vggt/depth | |||||
| env: | |||||
| iiwa14_primitive_collision_urdf: "iiwa14_description" | |||||
| iiwa14_primitive_collision_transform: .5 -0. -0.1 1. 0. 0. 0. | |||||
| CAMERA_PITCH: 1.5708 | |||||
| - id: gamepad | |||||
| build: pip install -e ../../node-hub/gamepad | |||||
| path: gamepad | |||||
| outputs: | |||||
| - cmd_vel | |||||
| - raw_control | |||||
| inputs: | |||||
| tick: dora/timer/millis/10 | |||||
| env: | |||||
| MAX_LINEAR_SPEED: 0.02 | |||||
| MAX_ANGULAR_SPEED: 0.10 | |||||
| - id: pytorch_kinematics | |||||
| build: pip install -e ../../node-hub/dora-pytorch-kinematics | |||||
| path: dora-pytorch-kinematics | |||||
| inputs: | |||||
| cmd_vel: gamepad/cmd_vel | |||||
| outputs: | |||||
| - cmd_vel | |||||
| env: | |||||
| MODEL_NAME: "iiwa14_description" | |||||
| END_EFFECTOR_LINK: "iiwa_link_7" | |||||
| TRANSFORM: .5 -0. -0.1 1. 0. 0. 0. | |||||
| - id: camera | |||||
| build: pip install -e ../../../node-hub/opencv-video-capture | |||||
| path: opencv-video-capture | |||||
| inputs: | |||||
| tick: dora/timer/millis/100 | |||||
| outputs: | |||||
| - image | |||||
| env: | |||||
| CAPTURE_PATH: 4 | |||||
| - id: camera2 | |||||
| build: pip install -e ../../../node-hub/opencv-video-capture | |||||
| path: opencv-video-capture | |||||
| inputs: | |||||
| tick: dora/timer/millis/100 | |||||
| outputs: | |||||
| - image | |||||
| env: | |||||
| CAPTURE_PATH: 6 | |||||
| - id: dora-vggt | |||||
| build: pip install -e ../../../node-hub/dora-vggt | |||||
| path: dora-vggt | |||||
| inputs: | |||||
| image: camera/image | |||||
| image2: camera2/image | |||||
| outputs: | |||||
| - depth | |||||
| - image | |||||
| env: | |||||
| SCALE_FACTOR: 0.9 | |||||
| @@ -0,0 +1,69 @@ | |||||
| nodes: | |||||
| - id: plot | |||||
| build: pip install -e ../../node-hub/dora-rerun | |||||
| path: dora-rerun | |||||
| inputs: | |||||
| jointstate_so101_new_calib: pytorch_kinematics/cmd_vel | |||||
| camera/image: dora-vggt/image | |||||
| camera/depth: dora-vggt/depth | |||||
| env: | |||||
| so101_new_calib_urdf: "so_arm101_description" | |||||
| so101_new_calib_transform: .14 -0. 0.4 -.5 .5 .5 -.5 | |||||
| - id: gamepad | |||||
| build: pip install -e ../../node-hub/gamepad | |||||
| path: gamepad | |||||
| outputs: | |||||
| - cmd_vel | |||||
| - raw_control | |||||
| inputs: | |||||
| tick: dora/timer/millis/10 | |||||
| env: | |||||
| MAX_LINEAR_SPEED: 0.01 | |||||
| MAX_ANGULAR_SPEED: 0.05 | |||||
| - id: pytorch_kinematics | |||||
| build: pip install -e ../../node-hub/dora-pytorch-kinematics | |||||
| path: dora-pytorch-kinematics | |||||
| inputs: | |||||
| cmd_vel: gamepad/cmd_vel | |||||
| outputs: | |||||
| - cmd_vel | |||||
| env: | |||||
| MODEL_NAME: "so_arm101_description" | |||||
| END_EFFECTOR_LINK: "gripper" | |||||
| TRANSFORM: .14 -0. 0.4 -.5 .5 .5 -.5 | |||||
| POSITION_TOLERANCE: 0.01 | |||||
| ROTATION_TOLERANCE: 0.03 | |||||
| - id: camera | |||||
| build: pip install -e ../../../node-hub/opencv-video-capture | |||||
| path: opencv-video-capture | |||||
| inputs: | |||||
| tick: dora/timer/millis/100 | |||||
| outputs: | |||||
| - image | |||||
| env: | |||||
| CAPTURE_PATH: 4 | |||||
| - id: camera2 | |||||
| build: pip install -e ../../../node-hub/opencv-video-capture | |||||
| path: opencv-video-capture | |||||
| inputs: | |||||
| tick: dora/timer/millis/100 | |||||
| outputs: | |||||
| - image | |||||
| env: | |||||
| CAPTURE_PATH: 6 | |||||
| - id: dora-vggt | |||||
| build: pip install -e ../../../node-hub/dora-vggt | |||||
| path: dora-vggt | |||||
| inputs: | |||||
| image: camera/image | |||||
| image2: camera2/image | |||||
| outputs: | |||||
| - depth | |||||
| - image | |||||
| env: | |||||
| SCALE_FACTOR: 0.9 | |||||
| @@ -0,0 +1,59 @@ | |||||
| nodes: | |||||
| - id: plot | |||||
| build: pip install -e ../../../node-hub/dora-rerun | |||||
| path: dora-rerun | |||||
| inputs: | |||||
| jointstate_z1: pytorch_kinematics/cmd_vel | |||||
| camera/image: dora-vggt/image | |||||
| camera/depth: dora-vggt/depth | |||||
| env: | |||||
| z1_urdf: z1_description | |||||
| z1_transform: .5 -0.2 -0.11 1. 0. 0. 0. | |||||
| CAMERA_PITCH: 1.5708 | |||||
| - id: gamepad | |||||
| build: pip install -e ../../../node-hub/gamepad | |||||
| path: gamepad | |||||
| outputs: | |||||
| - cmd_vel | |||||
| - raw_control | |||||
| inputs: | |||||
| tick: dora/timer/millis/10 | |||||
| env: | |||||
| MAX_LINEAR_SPEED: 0.01 | |||||
| MAX_ANGULAR_SPEED: 0.05 | |||||
| - id: pytorch_kinematics | |||||
| build: pip install -e ../../../node-hub/dora-pytorch-kinematics | |||||
| path: dora-pytorch-kinematics | |||||
| inputs: | |||||
| cmd_vel: gamepad/cmd_vel | |||||
| outputs: | |||||
| - cmd_vel | |||||
| env: | |||||
| MODEL_NAME: "z1_description" | |||||
| END_EFFECTOR_LINK: "link06" | |||||
| TRANSFORM: .5 -0.2 -0.11 1. 0. 0. 0. | |||||
| POSITION_TOLERANCE: 0.001 | |||||
| ROTATION_TOLERANCE: 0.001 | |||||
| - id: camera | |||||
| build: pip install -e ../../../node-hub/opencv-video-capture | |||||
| path: opencv-video-capture | |||||
| inputs: | |||||
| tick: dora/timer/millis/100 | |||||
| outputs: | |||||
| - image | |||||
| env: | |||||
| CAPTURE_PATH: 4 | |||||
| - id: dora-vggt | |||||
| build: pip install -e ../../../node-hub/dora-vggt | |||||
| path: dora-vggt | |||||
| inputs: | |||||
| image: camera/image | |||||
| outputs: | |||||
| - depth | |||||
| - image | |||||
| env: | |||||
| SCALE_FACTOR: 0.88 | |||||
| @@ -0,0 +1,54 @@ | |||||
| nodes: | |||||
| - id: camera | |||||
| build: pip install opencv-video-capture | |||||
| path: opencv-video-capture | |||||
| inputs: | |||||
| tick: dora/timer/millis/100 | |||||
| outputs: | |||||
| - image | |||||
| env: | |||||
| CAPTURE_PATH: 1 | |||||
| - id: dora-vggt | |||||
| build: pip install -e ../../node-hub/dora-vggt | |||||
| path: dora-vggt | |||||
| inputs: | |||||
| image: camera/image | |||||
| outputs: | |||||
| - depth | |||||
| - image | |||||
| env: | |||||
| DEPTH_ENCODING: mono16 | |||||
| - id: rav1e-depth | |||||
| path: dora-rav1e | |||||
| build: pip install -e ../../node-hub/dora-rav1e | |||||
| inputs: | |||||
| depth: dora-vggt/depth | |||||
| outputs: | |||||
| - depth | |||||
| env: | |||||
| ENCODING: avif | |||||
| - id: rav1e-image | |||||
| path: dora-rav1e | |||||
| build: pip install -e ../../node-hub/dora-rav1e | |||||
| inputs: | |||||
| image: dora-vggt/image | |||||
| outputs: | |||||
| - image | |||||
| env: | |||||
| ENCODING: avif | |||||
| - id: bench | |||||
| path: image_saver.py | |||||
| inputs: | |||||
| vggt_image: rav1e-image/image | |||||
| vggt_depth: rav1e-depth/depth | |||||
| - id: plot | |||||
| build: pip install dora-rerun | |||||
| path: dora-rerun | |||||
| inputs: | |||||
| camera/image: dora-vggt/image | |||||
| camera/depth: dora-vggt/depth | |||||
| @@ -1,8 +0,0 @@ | |||||
| build_id: 2b402c1e-e52e-45e9-86e5-236b33a77369 | |||||
| session_id: 275de19c-e605-4865-bc5f-2f15916bade9 | |||||
| git_sources: {} | |||||
| local_build: | |||||
| node_working_dirs: | |||||
| camera: /Users/xaviertao/Documents/work/dora/examples/vggt | |||||
| dora-vggt: /Users/xaviertao/Documents/work/dora/examples/vggt | |||||
| plot: /Users/xaviertao/Documents/work/dora/examples/vggt | |||||
| @@ -0,0 +1,34 @@ | |||||
| from dora import Node | |||||
| node = Node() | |||||
| index_dict = {} | |||||
| i = 0 | |||||
| LEAD_TOPIC = "vggt_depth" | |||||
| for event in node: | |||||
| if event["type"] == "INPUT": | |||||
| if LEAD_TOPIC in event["id"]: | |||||
| storage = event["value"] | |||||
| metadata = event["metadata"] | |||||
| encoding = metadata["encoding"] | |||||
| width = metadata["width"] | |||||
| height = metadata["height"] | |||||
| # Save to file | |||||
| filename = f"out/{event['id']}_{i}.{encoding}" | |||||
| with open(filename, "wb") as f: | |||||
| f.write(storage.to_numpy()) | |||||
| for key, value in index_dict.items(): | |||||
| filename = f"out/{key}_{i}.{value['metadata']['encoding']}" | |||||
| with open(filename, "wb") as f: | |||||
| f.write(value["value"]) | |||||
| i += 1 | |||||
| else: | |||||
| # Store the event in the index dictionary | |||||
| index_dict[event["id"]] = { | |||||
| "type": event["type"], | |||||
| "value": event["value"].to_numpy(), | |||||
| "metadata": event["metadata"], | |||||
| } | |||||
| @@ -0,0 +1,53 @@ | |||||
| nodes: | |||||
| - id: camera | |||||
| build: pip install -e ../../node-hub/dora-pyrealsense | |||||
| path: dora-pyrealsense | |||||
| inputs: | |||||
| tick: dora/timer/millis/100 | |||||
| outputs: | |||||
| - image | |||||
| - depth | |||||
| - id: dora-vggt | |||||
| build: pip install -e ../../node-hub/dora-vggt | |||||
| path: dora-vggt | |||||
| inputs: | |||||
| image: camera/image | |||||
| outputs: | |||||
| - depth | |||||
| - image | |||||
| env: | |||||
| DEPTH_ENCODING: mono16 | |||||
| - id: rav1e-depth-vggt | |||||
| path: dora-rav1e | |||||
| build: cargo build -p dora-rav1e --release | |||||
| inputs: | |||||
| depth: dora-vggt/depth | |||||
| outputs: | |||||
| - depth | |||||
| env: | |||||
| ENCODING: avif | |||||
| - id: rav1e-depth-realsense | |||||
| path: dora-rav1e | |||||
| build: cargo build -p dora-rav1e --release | |||||
| inputs: | |||||
| depth: camera/depth | |||||
| outputs: | |||||
| - depth | |||||
| env: | |||||
| ENCODING: avif | |||||
| - id: bench | |||||
| path: image_saver.py | |||||
| inputs: | |||||
| camera_depth: rav1e-depth-vggt/depth | |||||
| vggt_depth: rav1e-depth-realsense/depth | |||||
| - id: plot | |||||
| build: pip install dora-rerun | |||||
| path: dora-rerun | |||||
| inputs: | |||||
| camera/image: dora-vggt/image | |||||
| camera/depth: dora-vggt/depth | |||||
| @@ -54,7 +54,7 @@ pub fn check_dataflow( | |||||
| }; | }; | ||||
| } | } | ||||
| }, | }, | ||||
| dora_message::descriptor::NodeSource::GitBranch { repo, rev } => { | |||||
| dora_message::descriptor::NodeSource::GitBranch { .. } => { | |||||
| info!("skipping check for node with git source"); | info!("skipping check for node with git source"); | ||||
| } | } | ||||
| }, | }, | ||||
| @@ -6,7 +6,7 @@ use std::collections::HashMap; | |||||
| struct MetadataMap<'a>(HashMap<&'a str, &'a str>); | struct MetadataMap<'a>(HashMap<&'a str, &'a str>); | ||||
| impl<'a> Extractor for MetadataMap<'a> { | |||||
| impl Extractor for MetadataMap<'_> { | |||||
| /// Get a value for a key from the MetadataMap. If the value can't be converted to &str, returns None | /// Get a value for a key from the MetadataMap. If the value can't be converted to &str, returns None | ||||
| fn get(&self, key: &str) -> Option<&str> { | fn get(&self, key: &str) -> Option<&str> { | ||||
| self.0.get(key).cloned() | self.0.get(key).cloned() | ||||
| @@ -1,7 +1,7 @@ | |||||
| [package] | [package] | ||||
| name = "dora-message" | name = "dora-message" | ||||
| # versioned separately from the other dora crates | # versioned separately from the other dora crates | ||||
| version = "0.5.0-alpha" | |||||
| version = "0.5.0" | |||||
| edition.workspace = true | edition.workspace = true | ||||
| documentation.workspace = true | documentation.workspace = true | ||||
| description.workspace = true | description.workspace = true | ||||
| @@ -1,6 +1,6 @@ | |||||
| [project] | [project] | ||||
| name = "dora-argotranslate" | name = "dora-argotranslate" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| description = "Dora Node for Text translating using Argostranslate" | description = "Dora Node for Text translating using Argostranslate" | ||||
| authors = [ | authors = [ | ||||
| { name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }, | { name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }, | ||||
| @@ -1,6 +1,6 @@ | |||||
| [project] | [project] | ||||
| name = "dora-cotracker" | name = "dora-cotracker" | ||||
| version = "0.1.0" | |||||
| version = "0.3.12" | |||||
| authors = [{ name = "Shashwat Patil", email = "shashwatpatil974@gmail.com" }] | authors = [{ name = "Shashwat Patil", email = "shashwatpatil974@gmail.com" }] | ||||
| description = "A Dora node implementing real-time object tracking using Facebook's CoTracker model" | description = "A Dora node implementing real-time object tracking using Facebook's CoTracker model" | ||||
| license = "CC-BY-1.0" | license = "CC-BY-1.0" | ||||
| @@ -1,6 +1,6 @@ | |||||
| [project] | [project] | ||||
| name = "dora-distil-whisper" | name = "dora-distil-whisper" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| authors = [ | authors = [ | ||||
| { name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }, | { name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }, | ||||
| { name = "Enzo Le Van", email = "dev@enzo-le-van.fr" }, | { name = "Enzo Le Van", email = "dev@enzo-le-van.fr" }, | ||||
| @@ -1,6 +1,6 @@ | |||||
| [project] | [project] | ||||
| name = "dora-echo" | name = "dora-echo" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| authors = [ | authors = [ | ||||
| { name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }, | { name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }, | ||||
| { name = "Enzo Le Van", email = "dev@enzo-le-van.fr" }, | { name = "Enzo Le Van", email = "dev@enzo-le-van.fr" }, | ||||
| @@ -1,6 +1,6 @@ | |||||
| [project] | [project] | ||||
| name = "dora-gradio" | name = "dora-gradio" | ||||
| version = "0.2.0" | |||||
| version = "0.3.12" | |||||
| authors = [{ name = "Shashwat Patil", email = "email@email.com" }] | authors = [{ name = "Shashwat Patil", email = "email@email.com" }] | ||||
| description = "dora-gradio" | description = "dora-gradio" | ||||
| license = { text = "MIT" } | license = { text = "MIT" } | ||||
| @@ -1,6 +1,6 @@ | |||||
| [project] | [project] | ||||
| name = "dora-internvl" | name = "dora-internvl" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| authors = [ | authors = [ | ||||
| { name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }, | { name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }, | ||||
| { name = "Enzo Le Van", email = "dev@enzo-le-van.fr" }, | { name = "Enzo Le Van", email = "dev@enzo-le-van.fr" }, | ||||
| @@ -1,6 +1,6 @@ | |||||
| [project] | [project] | ||||
| name = "dora-ios-lidar" | name = "dora-ios-lidar" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| authors = [{ name = "Your Name", email = "email@email.com" }] | authors = [{ name = "Your Name", email = "email@email.com" }] | ||||
| description = "dora-ios-lidar" | description = "dora-ios-lidar" | ||||
| license = { text = "MIT" } | license = { text = "MIT" } | ||||
| @@ -1,6 +1,6 @@ | |||||
| [project] | [project] | ||||
| name = "dora-keyboard" | name = "dora-keyboard" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| authors = [ | authors = [ | ||||
| { name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }, | { name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }, | ||||
| { name = "Enzo Le Van", email = "dev@enzo-le-van.fr" }, | { name = "Enzo Le Van", email = "dev@enzo-le-van.fr" }, | ||||
| @@ -1,6 +1,6 @@ | |||||
| [project] | [project] | ||||
| name = "dora-kokoro-tts" | name = "dora-kokoro-tts" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| authors = [{ name = "Your Name", email = "email@email.com" }] | authors = [{ name = "Your Name", email = "email@email.com" }] | ||||
| description = "dora-kokoro-tts" | description = "dora-kokoro-tts" | ||||
| license = { text = "MIT" } | license = { text = "MIT" } | ||||
| @@ -1,6 +1,6 @@ | |||||
| [project] | [project] | ||||
| name = "dora-microphone" | name = "dora-microphone" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| authors = [ | authors = [ | ||||
| { name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }, | { name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }, | ||||
| { name = "Enzo Le Van", email = "dev@enzo-le-van.fr" }, | { name = "Enzo Le Van", email = "dev@enzo-le-van.fr" }, | ||||
| @@ -1,6 +1,6 @@ | |||||
| [package] | [package] | ||||
| name = "dora-object-to-pose" | name = "dora-object-to-pose" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| edition = "2021" | edition = "2021" | ||||
| # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||||
| @@ -1,6 +1,6 @@ | |||||
| [project] | [project] | ||||
| name = "dora-openai-server" | name = "dora-openai-server" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| authors = [ | authors = [ | ||||
| { name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }, | { name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }, | ||||
| { name = "Enzo Le Van", email = "dev@enzo-le-van.fr" }, | { name = "Enzo Le Van", email = "dev@enzo-le-van.fr" }, | ||||
| @@ -1,6 +1,6 @@ | |||||
| [project] | [project] | ||||
| name = "dora-opus" | name = "dora-opus" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| description = "Dora Node for Text translating using Opus" | description = "Dora Node for Text translating using Opus" | ||||
| authors = [ | authors = [ | ||||
| { name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }, | { name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }, | ||||
| @@ -1,6 +1,6 @@ | |||||
| [project] | [project] | ||||
| name = "dora-outtetts" | name = "dora-outtetts" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| authors = [] | authors = [] | ||||
| description = "dora-outtetts" | description = "dora-outtetts" | ||||
| license = { text = "MIT" } | license = { text = "MIT" } | ||||
| @@ -1,6 +1,6 @@ | |||||
| [project] | [project] | ||||
| name = "dora-parler" | name = "dora-parler" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| authors = [ | authors = [ | ||||
| { name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }, | { name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }, | ||||
| { name = "Enzo Le Van", email = "dev@enzo-le-van.fr" }, | { name = "Enzo Le Van", email = "dev@enzo-le-van.fr" }, | ||||
| @@ -1,6 +1,6 @@ | |||||
| [project] | [project] | ||||
| name = "dora-phi4" | name = "dora-phi4" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| authors = [{ name = "Somay", email = "ssomay2002@gmail.com" }] | authors = [{ name = "Somay", email = "ssomay2002@gmail.com" }] | ||||
| description = "DORA node for Phi-4 multimodal model" | description = "DORA node for Phi-4 multimodal model" | ||||
| license = { text = "MIT" } | license = { text = "MIT" } | ||||
| @@ -18,7 +18,6 @@ dependencies = [ | |||||
| "scipy==1.15.2", | "scipy==1.15.2", | ||||
| "backoff==2.2.1", | "backoff==2.2.1", | ||||
| "peft==0.13.2", | "peft==0.13.2", | ||||
| "bitsandbytes>=0.42.0", | |||||
| "opencv-python", | "opencv-python", | ||||
| "requests", | "requests", | ||||
| ] | ] | ||||
| @@ -1,6 +1,6 @@ | |||||
| [project] | [project] | ||||
| name = "dora-piper" | name = "dora-piper" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| authors = [{ name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }] | authors = [{ name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }] | ||||
| description = "Dora Node for using Agilex piper" | description = "Dora Node for using Agilex piper" | ||||
| license = { text = "MIT" } | license = { text = "MIT" } | ||||
| @@ -1,6 +1,6 @@ | |||||
| [project] | [project] | ||||
| name = "dora-pyaudio" | name = "dora-pyaudio" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| authors = [{ name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }] | authors = [{ name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }] | ||||
| license = { text = "MIT" } | license = { text = "MIT" } | ||||
| readme = "README.md" | readme = "README.md" | ||||
| @@ -1,6 +1,6 @@ | |||||
| [project] | [project] | ||||
| name = "dora-pyorbbecksdk" | name = "dora-pyorbbecksdk" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| authors = [ | authors = [ | ||||
| { name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }, | { name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }, | ||||
| { name = "Xiang Yang", email = "Ryu-Yang@qq.com" }, | { name = "Xiang Yang", email = "Ryu-Yang@qq.com" }, | ||||
| @@ -1,6 +1,6 @@ | |||||
| [project] | [project] | ||||
| name = "dora-pyrealsense" | name = "dora-pyrealsense" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| authors = [{ name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }] | authors = [{ name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }] | ||||
| description = "Dora Node for capturing video with Pyrealsense" | description = "Dora Node for capturing video with Pyrealsense" | ||||
| license = { text = "MIT" } | license = { text = "MIT" } | ||||
| @@ -1,6 +1,6 @@ | |||||
| [project] | [project] | ||||
| name = "dora-qwen" | name = "dora-qwen" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| authors = [{ name = "Your Name", email = "email@email.com" }] | authors = [{ name = "Your Name", email = "email@email.com" }] | ||||
| description = "dora-qwen" | description = "dora-qwen" | ||||
| license = { text = "MIT" } | license = { text = "MIT" } | ||||
| @@ -73,6 +73,10 @@ def generate( | |||||
| messages = [] | messages = [] | ||||
| # If the texts is string, convert it to a list | |||||
| if isinstance(texts, str): | |||||
| texts = [texts] | |||||
| for text in texts: | for text in texts: | ||||
| if text.startswith("<|system|>\n"): | if text.startswith("<|system|>\n"): | ||||
| messages.append( | messages.append( | ||||
| @@ -1,6 +1,6 @@ | |||||
| [project] | [project] | ||||
| name = "dora-qwen2-5-vl" | name = "dora-qwen2-5-vl" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12.post1" | |||||
| authors = [ | authors = [ | ||||
| { name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }, | { name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }, | ||||
| { name = "Enzo Le Van", email = "dev@enzo-le-van.fr" }, | { name = "Enzo Le Van", email = "dev@enzo-le-van.fr" }, | ||||
| @@ -1,6 +1,6 @@ | |||||
| [project] | [project] | ||||
| name = "dora-qwenvl" | name = "dora-qwenvl" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| authors = [ | authors = [ | ||||
| { name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }, | { name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }, | ||||
| { name = "Enzo Le Van", email = "dev@enzo-le-van.fr" }, | { name = "Enzo Le Van", email = "dev@enzo-le-van.fr" }, | ||||
| @@ -1,7 +1,7 @@ | |||||
| [package] | [package] | ||||
| name = "dora-rav1e" | name = "dora-rav1e" | ||||
| edition = "2021" | edition = "2021" | ||||
| version = "0.3.11+fix1" | |||||
| version = "0.3.12" | |||||
| description.workspace = true | description.workspace = true | ||||
| documentation.workspace = true | documentation.workspace = true | ||||
| license = "BSD-2-Clause" | license = "BSD-2-Clause" | ||||
| @@ -25,7 +25,8 @@ pyo3 = { workspace = true, features = [ | |||||
| "eyre", | "eyre", | ||||
| "generate-import-lib", | "generate-import-lib", | ||||
| ], optional = true } | ], optional = true } | ||||
| avif-serialize = "0.8.3" | |||||
| avif-serialize = { version = "0.8.5" } | |||||
| little_exif = { version = "0.6.9" } | |||||
| [lib] | [lib] | ||||
| @@ -8,12 +8,16 @@ | |||||
| // PATENTS file, you can obtain it at www.aomedia.org/license/patent. | // PATENTS file, you can obtain it at www.aomedia.org/license/patent. | ||||
| use std::env::var; | use std::env::var; | ||||
| use std::vec; | |||||
| use dora_node_api::arrow::array::AsArray; | use dora_node_api::arrow::array::AsArray; | ||||
| use dora_node_api::arrow::datatypes::{UInt16Type, UInt8Type}; | use dora_node_api::arrow::datatypes::{UInt16Type, UInt8Type}; | ||||
| use dora_node_api::dora_core::config::DataId; | use dora_node_api::dora_core::config::DataId; | ||||
| use dora_node_api::{DoraNode, Event, IntoArrow, Metadata, Parameter}; | |||||
| use dora_node_api::{DoraNode, Event, IntoArrow, Metadata, MetadataParameters, Parameter}; | |||||
| use eyre::{Context as EyreContext, Result}; | use eyre::{Context as EyreContext, Result}; | ||||
| use little_exif::exif_tag::ExifTag; | |||||
| use little_exif::metadata::Metadata as ExifMetadata; | |||||
| use little_exif::rational::uR64; | |||||
| use log::warn; | use log::warn; | ||||
| use rav1e::color::{ColorDescription, MatrixCoefficients}; | use rav1e::color::{ColorDescription, MatrixCoefficients}; | ||||
| // Encode the same tiny blank frame 30 times | // Encode the same tiny blank frame 30 times | ||||
| @@ -56,6 +60,25 @@ pub fn fill_zeros_toward_center_y_plane_in_place(y: &mut [u16], width: usize, he | |||||
| } | } | ||||
| } | } | ||||
| fn metadata_to_exif(metadata: &MetadataParameters) -> Result<Vec<u8>> { | |||||
| let mut metadata_exif = ExifMetadata::new(); | |||||
| metadata_exif.set_tag(ExifTag::Software("dora-rs".to_string())); | |||||
| if let Some(Parameter::ListInt(focal_lengths)) = metadata.get("focal") { | |||||
| metadata_exif.set_tag(ExifTag::FocalLength( | |||||
| focal_lengths | |||||
| .iter() | |||||
| .map(|&f| uR64 { | |||||
| nominator: f as u32, | |||||
| denominator: 1, | |||||
| }) | |||||
| .collect::<Vec<_>>(), | |||||
| )); | |||||
| } | |||||
| let vector = metadata_exif.as_u8_vec(little_exif::filetype::FileExtension::HEIF)?; | |||||
| return Ok(vector); | |||||
| } | |||||
| fn bgr8_to_yuv420(bgr_data: Vec<u8>, width: usize, height: usize) -> (Vec<u8>, Vec<u8>, Vec<u8>) { | fn bgr8_to_yuv420(bgr_data: Vec<u8>, width: usize, height: usize) -> (Vec<u8>, Vec<u8>, Vec<u8>) { | ||||
| let mut y_plane = vec![0; width * height]; | let mut y_plane = vec![0; width * height]; | ||||
| let mut u_plane = vec![0; (width / 2) * (height / 2)]; | let mut u_plane = vec![0; (width / 2) * (height / 2)]; | ||||
| @@ -107,6 +130,7 @@ fn get_yuv_planes(buffer: &[u8], width: usize, height: usize) -> (&[u8], &[u8], | |||||
| (y_plane, u_plane, v_plane) | (y_plane, u_plane, v_plane) | ||||
| } | } | ||||
| #[allow(clippy::too_many_arguments)] | |||||
| fn send_yuv( | fn send_yuv( | ||||
| y: &[u8], | y: &[u8], | ||||
| u: &[u8], | u: &[u8], | ||||
| @@ -118,7 +142,7 @@ fn send_yuv( | |||||
| id: DataId, | id: DataId, | ||||
| metadata: &mut Metadata, | metadata: &mut Metadata, | ||||
| output_encoding: &str, | output_encoding: &str, | ||||
| ) -> () { | |||||
| ) { | |||||
| // Create a new Arrow array for the YUV420 data | // Create a new Arrow array for the YUV420 data | ||||
| let cfg = Config::new().with_encoder_config(enc.clone()); | let cfg = Config::new().with_encoder_config(enc.clone()); | ||||
| let mut ctx: Context<u8> = cfg.new_context().unwrap(); | let mut ctx: Context<u8> = cfg.new_context().unwrap(); | ||||
| @@ -126,13 +150,13 @@ fn send_yuv( | |||||
| let xdec = f.planes[0].cfg.xdec; | let xdec = f.planes[0].cfg.xdec; | ||||
| let stride = (width + xdec) >> xdec; | let stride = (width + xdec) >> xdec; | ||||
| f.planes[0].copy_from_raw_u8(&y, stride, 1); | |||||
| f.planes[0].copy_from_raw_u8(y, stride, 1); | |||||
| let xdec = f.planes[1].cfg.xdec; | let xdec = f.planes[1].cfg.xdec; | ||||
| let stride = (width + xdec) >> xdec; | let stride = (width + xdec) >> xdec; | ||||
| f.planes[1].copy_from_raw_u8(&u, stride, 1); | |||||
| f.planes[1].copy_from_raw_u8(u, stride, 1); | |||||
| let xdec = f.planes[2].cfg.xdec; | let xdec = f.planes[2].cfg.xdec; | ||||
| let stride = (width + xdec) >> xdec; | let stride = (width + xdec) >> xdec; | ||||
| f.planes[2].copy_from_raw_u8(&v, stride, 1); | |||||
| f.planes[2].copy_from_raw_u8(v, stride, 1); | |||||
| match ctx.send_frame(f) { | match ctx.send_frame(f) { | ||||
| Ok(_) => {} | Ok(_) => {} | ||||
| @@ -159,9 +183,18 @@ fn send_yuv( | |||||
| } else { | } else { | ||||
| MatrixCoefficients::BT709 | MatrixCoefficients::BT709 | ||||
| }; | }; | ||||
| let data = avif_serialize::Aviffy::new() | |||||
| let mut aviffy = avif_serialize::Aviffy::new(); | |||||
| aviffy | |||||
| .set_chroma_subsampling((true, true)) | .set_chroma_subsampling((true, true)) | ||||
| .set_seq_profile(0) | |||||
| .set_seq_profile(0); | |||||
| let aviffy = if let Ok(exif) = metadata_to_exif(&metadata.parameters) { | |||||
| aviffy.set_exif(exif) | |||||
| } else { | |||||
| &mut aviffy | |||||
| }; | |||||
| let data = aviffy | |||||
| .matrix_coefficients(match matrix_coefficients { | .matrix_coefficients(match matrix_coefficients { | ||||
| MatrixCoefficients::Identity => { | MatrixCoefficients::Identity => { | ||||
| avif_serialize::constants::MatrixCoefficients::Rgb | avif_serialize::constants::MatrixCoefficients::Rgb | ||||
| @@ -289,12 +322,9 @@ pub fn lib_main() -> Result<()> { | |||||
| chroma_sampling: color::ChromaSampling::Cs420, | chroma_sampling: color::ChromaSampling::Cs420, | ||||
| ..Default::default() | ..Default::default() | ||||
| }; | }; | ||||
| match encoding { | |||||
| "mono16" => { | |||||
| enc.bit_depth = 12; | |||||
| enc.chroma_sampling = color::ChromaSampling::Cs400; | |||||
| } | |||||
| _ => {} | |||||
| if encoding == "mono16" { | |||||
| enc.bit_depth = 12; | |||||
| enc.chroma_sampling = color::ChromaSampling::Cs400; | |||||
| } | } | ||||
| if encoding == "bgr8" { | if encoding == "bgr8" { | ||||
| @@ -320,9 +350,9 @@ pub fn lib_main() -> Result<()> { | |||||
| let (y, u, v) = get_yuv_planes(buffer, width, height); | let (y, u, v) = get_yuv_planes(buffer, width, height); | ||||
| send_yuv( | send_yuv( | ||||
| &y, | |||||
| &u, | |||||
| &v, | |||||
| y, | |||||
| u, | |||||
| v, | |||||
| enc, | enc, | ||||
| width, | width, | ||||
| height, | height, | ||||
| @@ -336,13 +366,13 @@ pub fn lib_main() -> Result<()> { | |||||
| if let Some(buffer) = data.as_primitive_opt::<UInt16Type>() { | if let Some(buffer) = data.as_primitive_opt::<UInt16Type>() { | ||||
| let mut buffer = buffer.values().to_vec(); | let mut buffer = buffer.values().to_vec(); | ||||
| if std::env::var("FILL_ZEROS") | if std::env::var("FILL_ZEROS") | ||||
| .map(|s| s != "false") | |||||
| .map(|s| s.to_lowercase() != "false") | |||||
| .unwrap_or(true) | .unwrap_or(true) | ||||
| { | { | ||||
| fill_zeros_toward_center_y_plane_in_place(&mut buffer, width, height); | fill_zeros_toward_center_y_plane_in_place(&mut buffer, width, height); | ||||
| } | } | ||||
| let bytes: &[u8] = &bytemuck::cast_slice(&buffer); | |||||
| let bytes: &[u8] = bytemuck::cast_slice(&buffer); | |||||
| let cfg = Config::new().with_encoder_config(enc.clone()); | let cfg = Config::new().with_encoder_config(enc.clone()); | ||||
| let mut ctx: Context<u16> = cfg.new_context().unwrap(); | let mut ctx: Context<u16> = cfg.new_context().unwrap(); | ||||
| @@ -370,7 +400,38 @@ pub fn lib_main() -> Result<()> { | |||||
| let data = pkt.data; | let data = pkt.data; | ||||
| match output_encoding.as_str() { | match output_encoding.as_str() { | ||||
| "avif" => { | "avif" => { | ||||
| warn!("avif encoding not supported for mono16"); | |||||
| metadata.parameters.insert( | |||||
| "encoding".to_string(), | |||||
| Parameter::String("avif".to_string()), | |||||
| ); | |||||
| let mut aviffy = avif_serialize::Aviffy::new(); | |||||
| aviffy | |||||
| .full_color_range(false) | |||||
| .set_seq_profile(0) | |||||
| .set_monochrome(true); | |||||
| let aviffy = if let Ok(exif) = | |||||
| metadata_to_exif(&metadata.parameters) | |||||
| { | |||||
| aviffy.set_exif(exif) | |||||
| } else { | |||||
| &mut aviffy | |||||
| }; | |||||
| let data = aviffy.to_vec( | |||||
| &data, | |||||
| None, | |||||
| enc.width as u32, | |||||
| enc.height as u32, | |||||
| enc.bit_depth as u8, | |||||
| ); | |||||
| let arrow = data.into_arrow(); | |||||
| node.send_output(id, metadata.parameters.clone(), arrow) | |||||
| .context("could not send output") | |||||
| .unwrap(); | |||||
| } | } | ||||
| _ => { | _ => { | ||||
| metadata.parameters.insert( | metadata.parameters.insert( | ||||
| @@ -1,6 +1,6 @@ | |||||
| [project] | [project] | ||||
| name = "dora-rdt-1b" | name = "dora-rdt-1b" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| authors = [{ name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }] | authors = [{ name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }] | ||||
| description = "Dora Node for RDT 1B" | description = "Dora Node for RDT 1B" | ||||
| license = { text = "MIT" } | license = { text = "MIT" } | ||||
| @@ -1,6 +1,6 @@ | |||||
| [project] | [project] | ||||
| name = "dora-reachy2" | name = "dora-reachy2" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| authors = [{ name = "Your Name", email = "email@email.com" }] | authors = [{ name = "Your Name", email = "email@email.com" }] | ||||
| description = "dora-reachy2" | description = "dora-reachy2" | ||||
| license = { text = "MIT" } | license = { text = "MIT" } | ||||
| @@ -160,8 +160,7 @@ pub fn lib_main() -> Result<()> { | |||||
| let buffer: Vec<u8> = | let buffer: Vec<u8> = | ||||
| buffer.chunks(3).flat_map(|x| [x[2], x[1], x[0]]).collect(); | buffer.chunks(3).flat_map(|x| [x[2], x[1], x[0]]).collect(); | ||||
| image_cache.insert(id.clone(), buffer.clone()); | image_cache.insert(id.clone(), buffer.clone()); | ||||
| let image_buffer = ImageBuffer::try_from(buffer) | |||||
| .context("Could not convert buffer to image buffer")?; | |||||
| let image_buffer = ImageBuffer::from(buffer); | |||||
| // let tensordata = ImageBuffer(buffer); | // let tensordata = ImageBuffer(buffer); | ||||
| let image = rerun::Image::new( | let image = rerun::Image::new( | ||||
| @@ -174,8 +173,7 @@ pub fn lib_main() -> Result<()> { | |||||
| let buffer: &UInt8Array = data.as_any().downcast_ref().unwrap(); | let buffer: &UInt8Array = data.as_any().downcast_ref().unwrap(); | ||||
| image_cache.insert(id.clone(), buffer.values().to_vec()); | image_cache.insert(id.clone(), buffer.values().to_vec()); | ||||
| let buffer: &[u8] = buffer.values(); | let buffer: &[u8] = buffer.values(); | ||||
| let image_buffer = ImageBuffer::try_from(buffer) | |||||
| .context("Could not convert buffer to image buffer")?; | |||||
| let image_buffer = ImageBuffer::from(buffer); | |||||
| let image = rerun::Image::new( | let image = rerun::Image::new( | ||||
| image_buffer, | image_buffer, | ||||
| @@ -385,12 +383,12 @@ pub fn lib_main() -> Result<()> { | |||||
| // Get color or assign random color in cache | // Get color or assign random color in cache | ||||
| let color = color_cache.get(&id); | let color = color_cache.get(&id); | ||||
| let color = if let Some(color) = color { | let color = if let Some(color) = color { | ||||
| color.clone() | |||||
| *color | |||||
| } else { | } else { | ||||
| let color = | let color = | ||||
| rerun::Color::from_rgb(rand::random::<u8>(), 180, rand::random::<u8>()); | rerun::Color::from_rgb(rand::random::<u8>(), 180, rand::random::<u8>()); | ||||
| color_cache.insert(id.clone(), color.clone()); | |||||
| color_cache.insert(id.clone(), color); | |||||
| color | color | ||||
| }; | }; | ||||
| let dataid = id; | let dataid = id; | ||||
| @@ -412,12 +410,12 @@ pub fn lib_main() -> Result<()> { | |||||
| // Get color or assign random color in cache | // Get color or assign random color in cache | ||||
| let color = color_cache.get(&id); | let color = color_cache.get(&id); | ||||
| let color = if let Some(color) = color { | let color = if let Some(color) = color { | ||||
| color.clone() | |||||
| *color | |||||
| } else { | } else { | ||||
| let color = | let color = | ||||
| rerun::Color::from_rgb(rand::random::<u8>(), 180, rand::random::<u8>()); | rerun::Color::from_rgb(rand::random::<u8>(), 180, rand::random::<u8>()); | ||||
| color_cache.insert(id.clone(), color.clone()); | |||||
| color_cache.insert(id.clone(), color); | |||||
| color | color | ||||
| }; | }; | ||||
| let dataid = id; | let dataid = id; | ||||
| @@ -7,7 +7,7 @@ pub fn update_series(rec: &RecordingStream, id: DataId, data: ArrowData) -> Resu | |||||
| for (i, value) in series.iter().enumerate() { | for (i, value) in series.iter().enumerate() { | ||||
| rec.log( | rec.log( | ||||
| format!("{}_{}", id.as_str(), i), | format!("{}_{}", id.as_str(), i), | ||||
| &rerun::Scalar::new(*value as f64), | |||||
| &rerun::Scalars::new([*value]), | |||||
| ) | ) | ||||
| .wrap_err("could not log series")?; | .wrap_err("could not log series")?; | ||||
| } | } | ||||
| @@ -1,4 +1,4 @@ | |||||
| use std::{collections::HashMap, fmt::format, path::PathBuf}; | |||||
| use std::{collections::HashMap, path::PathBuf}; | |||||
| use eyre::{Context, ContextCompat, Result}; | use eyre::{Context, ContextCompat, Result}; | ||||
| use k::{nalgebra::Quaternion, Chain, Translation3, UnitQuaternion}; | use k::{nalgebra::Quaternion, Chain, Translation3, UnitQuaternion}; | ||||
| @@ -44,12 +44,13 @@ pub fn lib_main() -> Result<()> { | |||||
| } | } | ||||
| while let Some(event) = events.recv() { | while let Some(event) = events.recv() { | ||||
| match event { | |||||
| Event::Input { | |||||
| id, | |||||
| metadata: _, | |||||
| data, | |||||
| } => match id.as_str() { | |||||
| if let Event::Input { | |||||
| id, | |||||
| metadata: _, | |||||
| data, | |||||
| } = event | |||||
| { | |||||
| match id.as_str() { | |||||
| "tick" => { | "tick" => { | ||||
| if let Ok(joints) = c.read_present_position(&ids) { | if let Ok(joints) = c.read_present_position(&ids) { | ||||
| let mut parameter = BTreeMap::new(); | let mut parameter = BTreeMap::new(); | ||||
| @@ -70,8 +71,7 @@ pub fn lib_main() -> Result<()> { | |||||
| c.write_goal_position(&ids, &data).unwrap(); | c.write_goal_position(&ids, &data).unwrap(); | ||||
| } | } | ||||
| other => eprintln!("Received input `{other}`"), | other => eprintln!("Received input `{other}`"), | ||||
| }, | |||||
| _ => {} | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -1,6 +1,6 @@ | |||||
| [project] | [project] | ||||
| name = "dora-sam2" | name = "dora-sam2" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| authors = [{ name = "Your Name", email = "email@email.com" }] | authors = [{ name = "Your Name", email = "email@email.com" }] | ||||
| description = "dora-sam2" | description = "dora-sam2" | ||||
| license = { text = "MIT" } | license = { text = "MIT" } | ||||
| @@ -1,6 +1,6 @@ | |||||
| [project] | [project] | ||||
| name = "dora-ugv" | name = "dora-ugv" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| authors = [{ name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }] | authors = [{ name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }] | ||||
| description = "Dora Node for using Agilex UGV" | description = "Dora Node for using Agilex UGV" | ||||
| license = { text = "MIT" } | license = { text = "MIT" } | ||||
| @@ -1,6 +1,6 @@ | |||||
| [project] | [project] | ||||
| name = "dora-vad" | name = "dora-vad" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| description = "Dora Node for Text translating using Argostranslate" | description = "Dora Node for Text translating using Argostranslate" | ||||
| authors = [{ name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }] | authors = [{ name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }] | ||||
| license = { text = "MIT" } | license = { text = "MIT" } | ||||
| @@ -1,7 +1,8 @@ | |||||
| """TODO: Add docstring.""" | """TODO: Add docstring.""" | ||||
| import io | import io | ||||
| from collections import deque as Deque | |||||
| import os | |||||
| from collections import deque | |||||
| import cv2 | import cv2 | ||||
| import numpy as np | import numpy as np | ||||
| @@ -13,26 +14,31 @@ from vggt.models.vggt import VGGT | |||||
| from vggt.utils.load_fn import load_and_preprocess_images | from vggt.utils.load_fn import load_and_preprocess_images | ||||
| from vggt.utils.pose_enc import pose_encoding_to_extri_intri | from vggt.utils.pose_enc import pose_encoding_to_extri_intri | ||||
| SCALE_FACTOR = float(os.getenv("SCALE_FACTOR", "1")) | |||||
| VGGT_NUM_IMAGES = int(os.getenv("VGGT_NUM_IMAGES", "2")) | |||||
| # bfloat16 is supported on Ampere GPUs (Compute Capability 8.0+) | # bfloat16 is supported on Ampere GPUs (Compute Capability 8.0+) | ||||
| dtype = torch.bfloat16 | dtype = torch.bfloat16 | ||||
| # Check if cuda is available and set the device accordingly | |||||
| device = "cuda" if torch.cuda.is_available() else "cpu" | |||||
| # Initialize the model and load the pretrained weights. | # Initialize the model and load the pretrained weights. | ||||
| # This will automatically download the model weights the first time it's run, which may take a while. | # This will automatically download the model weights the first time it's run, which may take a while. | ||||
| model = VGGT.from_pretrained("facebook/VGGT-1B").to("cuda") | |||||
| model = VGGT.from_pretrained("facebook/VGGT-1B").to(device) | |||||
| model.eval() | model.eval() | ||||
| DEPTH_ENCODING = os.environ.get("DEPTH_ENCODING", "float64") | |||||
| # Import vecdeque | # Import vecdeque | ||||
| def main(): | def main(): | ||||
| """TODO: Add docstring.""" | """TODO: Add docstring.""" | ||||
| node = Node() | node = Node() | ||||
| raw_images = Deque(maxlen=2) | |||||
| raw_images = deque(maxlen=VGGT_NUM_IMAGES) | |||||
| for event in node: | for event in node: | ||||
| if event["type"] == "INPUT": | if event["type"] == "INPUT": | ||||
| if "image" in event["id"]: | if "image" in event["id"]: | ||||
| storage = event["value"] | storage = event["value"] | ||||
| metadata = event["metadata"] | metadata = event["metadata"] | ||||
| @@ -80,7 +86,7 @@ def main(): | |||||
| raw_images.append(buffer) | raw_images.append(buffer) | ||||
| with torch.no_grad(): | with torch.no_grad(): | ||||
| images = load_and_preprocess_images(raw_images).to("cuda") | |||||
| images = load_and_preprocess_images(raw_images).to(device) | |||||
| images = images[None] # add batch dimension | images = images[None] # add batch dimension | ||||
| aggregated_tokens_list, ps_idx = model.aggregator(images) | aggregated_tokens_list, ps_idx = model.aggregator(images) | ||||
| @@ -88,7 +94,7 @@ def main(): | |||||
| pose_enc = model.camera_head(aggregated_tokens_list)[-1] | pose_enc = model.camera_head(aggregated_tokens_list)[-1] | ||||
| # Extrinsic and intrinsic matrices, following OpenCV convention (camera from world) | # Extrinsic and intrinsic matrices, following OpenCV convention (camera from world) | ||||
| extrinsic, intrinsic = pose_encoding_to_extri_intri( | extrinsic, intrinsic = pose_encoding_to_extri_intri( | ||||
| pose_enc, images.shape[-2:] | |||||
| pose_enc, images.shape[-2:], | |||||
| ) | ) | ||||
| intrinsic = intrinsic[-1][-1] | intrinsic = intrinsic[-1][-1] | ||||
| f_0 = intrinsic[0, 0] | f_0 = intrinsic[0, 0] | ||||
| @@ -98,29 +104,32 @@ def main(): | |||||
| # Predict Depth Maps | # Predict Depth Maps | ||||
| depth_map, depth_conf = model.depth_head( | depth_map, depth_conf = model.depth_head( | ||||
| aggregated_tokens_list, images, ps_idx | |||||
| aggregated_tokens_list, images, ps_idx, | |||||
| ) | ) | ||||
| print(depth_conf.max()) | |||||
| depth_map[depth_conf < 1.0] = 0.0 # Set low confidence pixels to 0 | depth_map[depth_conf < 1.0] = 0.0 # Set low confidence pixels to 0 | ||||
| depth_map = depth_map.to(torch.float64) | depth_map = depth_map.to(torch.float64) | ||||
| depth_map = depth_map[-1][-1].cpu().numpy() | depth_map = depth_map[-1][-1].cpu().numpy() | ||||
| depth_map = SCALE_FACTOR * depth_map | |||||
| # Warning: Make sure to add my_output_id and my_input_id within the dataflow. | # Warning: Make sure to add my_output_id and my_input_id within the dataflow. | ||||
| if DEPTH_ENCODING == "mono16": | |||||
| depth_map = (depth_map * 1000).astype(np.uint16) | |||||
| node.send_output( | node.send_output( | ||||
| output_id="depth", | |||||
| output_id=event["id"].replace("image", "depth"), | |||||
| data=pa.array(depth_map.ravel()), | data=pa.array(depth_map.ravel()), | ||||
| metadata={ | metadata={ | ||||
| "width": depth_map.shape[1], | "width": depth_map.shape[1], | ||||
| "height": depth_map.shape[0], | "height": depth_map.shape[0], | ||||
| "focal": [ | |||||
| int(f_0), | |||||
| int(f_1), | |||||
| ], | |||||
| "resolution": [ | |||||
| int(r_0), | |||||
| int(r_1), | |||||
| ], | |||||
| "encoding": DEPTH_ENCODING, | |||||
| "focal": [ | |||||
| int(f_0), | |||||
| int(f_1), | |||||
| ], | |||||
| "resolution": [ | |||||
| int(r_0), | |||||
| int(r_1), | |||||
| ], | |||||
| }, | }, | ||||
| ) | ) | ||||
| @@ -129,18 +138,22 @@ def main(): | |||||
| # reorder pixels to be in last dimension | # reorder pixels to be in last dimension | ||||
| image = image.transpose(1, 2, 0) | image = image.transpose(1, 2, 0) | ||||
| print( | |||||
| f"Image shape: {image.shape}, dtype: {image.dtype} and depth map shape: {depth_map.shape}, dtype: {depth_map.dtype}" | |||||
| ) | |||||
| # Warning: Make sure to add my_output_id and my_input_id within the dataflow. | # Warning: Make sure to add my_output_id and my_input_id within the dataflow. | ||||
| node.send_output( | node.send_output( | ||||
| output_id="image", | |||||
| output_id=event["id"], | |||||
| data=pa.array(image.ravel()), | data=pa.array(image.ravel()), | ||||
| metadata={ | metadata={ | ||||
| "encoding": "rgb8", | "encoding": "rgb8", | ||||
| "width": image.shape[1], | "width": image.shape[1], | ||||
| "height": image.shape[0], | "height": image.shape[0], | ||||
| "focal": [ | |||||
| int(f_0), | |||||
| int(f_1), | |||||
| ], | |||||
| "resolution": [ | |||||
| int(r_0), | |||||
| int(r_1), | |||||
| ], | |||||
| }, | }, | ||||
| ) | ) | ||||
| @@ -1,6 +1,6 @@ | |||||
| [project] | [project] | ||||
| name = "dora-yolo" | name = "dora-yolo" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| authors = [ | authors = [ | ||||
| { name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }, | { name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }, | ||||
| { name = "Enzo Le Van", email = "dev@enzo-le-van.fr" }, | { name = "Enzo Le Van", email = "dev@enzo-le-van.fr" }, | ||||
| @@ -8,7 +8,7 @@ readme = "README.md" | |||||
| requires-python = ">=3.9" | requires-python = ">=3.9" | ||||
| dependencies = [ | dependencies = [ | ||||
| "dora-rs == 0.3.11", | |||||
| "dora-rs == 0.3.12", | |||||
| "numpy <= 2.0.0", | "numpy <= 2.0.0", | ||||
| "feetech-servo-sdk == 1.0.0", | "feetech-servo-sdk == 1.0.0", | ||||
| "pwm-position-control", | "pwm-position-control", | ||||
| @@ -1,6 +1,6 @@ | |||||
| [project] | [project] | ||||
| name = "gamepad" | name = "gamepad" | ||||
| version = "0.1.0" | |||||
| version = "0.3.12" | |||||
| authors = [{ name = "Shashwat Patil", email = "email@email.com" }] | authors = [{ name = "Shashwat Patil", email = "email@email.com" }] | ||||
| description = "gamepad" | description = "gamepad" | ||||
| license = { text = "MIT" } | license = { text = "MIT" } | ||||
| @@ -1,6 +1,6 @@ | |||||
| [project] | [project] | ||||
| name = "llama-factory-recorder" | name = "llama-factory-recorder" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| authors = [ | authors = [ | ||||
| { name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }, | { name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }, | ||||
| { name = "Enzo Le Van", email = "dev@enzo-le-van.fr" }, | { name = "Enzo Le Van", email = "dev@enzo-le-van.fr" }, | ||||
| @@ -96,7 +96,7 @@ async fn main() -> eyre::Result<()> { | |||||
| ) | ) | ||||
| .context("failed to send dora output")?; | .context("failed to send dora output")?; | ||||
| reply_channels.push_back((reply, 0 as u64, request.model)); | |||||
| reply_channels.push_back((reply, 0_u64, request.model)); | |||||
| } | } | ||||
| }, | }, | ||||
| dora_node_api::merged::MergedEvent::Dora(event) => match event { | dora_node_api::merged::MergedEvent::Dora(event) => match event { | ||||
| @@ -112,7 +112,7 @@ async fn main() -> eyre::Result<()> { | |||||
| let data = data.as_string::<i32>(); | let data = data.as_string::<i32>(); | ||||
| let string = data.iter().fold("".to_string(), |mut acc, s| { | let string = data.iter().fold("".to_string(), |mut acc, s| { | ||||
| if let Some(s) = s { | if let Some(s) = s { | ||||
| acc.push_str("\n"); | |||||
| acc.push('\n'); | |||||
| acc.push_str(s); | acc.push_str(s); | ||||
| } | } | ||||
| acc | acc | ||||
| @@ -164,6 +164,7 @@ async fn main() -> eyre::Result<()> { | |||||
| Ok(()) | Ok(()) | ||||
| } | } | ||||
| #[allow(clippy::large_enum_variant)] | |||||
| enum ServerEvent { | enum ServerEvent { | ||||
| Result(eyre::Result<()>), | Result(eyre::Result<()>), | ||||
| ChatCompletionRequest { | ChatCompletionRequest { | ||||
| @@ -1,6 +1,6 @@ | |||||
| [project] | [project] | ||||
| name = "opencv-plot" | name = "opencv-plot" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| license = { file = "MIT" } | license = { file = "MIT" } | ||||
| authors = [ | authors = [ | ||||
| { name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }, | { name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }, | ||||
| @@ -1,6 +1,6 @@ | |||||
| [project] | [project] | ||||
| name = "opencv-video-capture" | name = "opencv-video-capture" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| authors = [ | authors = [ | ||||
| { name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }, | { name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }, | ||||
| { name = "Enzo Le Van", email = "dev@enzo-le-van.fr" }, | { name = "Enzo Le Van", email = "dev@enzo-le-van.fr" }, | ||||
| @@ -1,6 +1,6 @@ | |||||
| [project] | [project] | ||||
| name = "pyarrow-assert" | name = "pyarrow-assert" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| authors = [ | authors = [ | ||||
| { name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }, | { name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }, | ||||
| { name = "Enzo Le Van", email = "dev@enzo-le-van.fr" }, | { name = "Enzo Le Van", email = "dev@enzo-le-van.fr" }, | ||||
| @@ -1,6 +1,6 @@ | |||||
| [project] | [project] | ||||
| name = "pyarrow-sender" | name = "pyarrow-sender" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| authors = [ | authors = [ | ||||
| { name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }, | { name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }, | ||||
| { name = "Enzo Le Van", email = "dev@enzo-le-van.fr" }, | { name = "Enzo Le Van", email = "dev@enzo-le-van.fr" }, | ||||
| @@ -1,6 +1,6 @@ | |||||
| [project] | [project] | ||||
| name = "terminal-input" | name = "terminal-input" | ||||
| version = "0.3.11" | |||||
| version = "0.3.12" | |||||
| authors = [ | authors = [ | ||||
| { name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }, | { name = "Haixuan Xavier Tao", email = "tao.xavier@outlook.com" }, | ||||
| { name = "Enzo Le Van", email = "dev@enzo-le-van.fr" }, | { name = "Enzo Le Van", email = "dev@enzo-le-van.fr" }, | ||||