diff --git a/.gitignore b/.gitignore
index c41d8ef..40580f3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,3 +25,6 @@ test_performance
publish_camera_tf.py
tailscale/
target
+stress_test
+.robonix
+rust/debug
\ No newline at end of file
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 8564050..21c8be1 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -37,7 +37,9 @@ RUN apt update && apt install -y \
libglfw3 \
libglfw3-dev \
mesa-utils \
- libclang-dev python3-vcstool
+ libclang-dev python3-vcstool \
+ bash-completion \
+ fonts-lmodern
# RUN apt update && apt install -y curl && \
# curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/focal.noarmor.gpg | tee /usr/share/keyrings/tailscale-archive-keyring.gpg >/dev/null && \
diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh
index f525458..87f5b6f 100755
--- a/docker/docker-entrypoint.sh
+++ b/docker/docker-entrypoint.sh
@@ -17,6 +17,21 @@ source /opt/ros/humble/setup.bash
echo "source /opt/ros/humble/setup.bash" >> ~/.bashrc
+# Enable bash completion
+if [ -f /usr/share/bash-completion/bash_completion ]; then
+ . /usr/share/bash-completion/bash_completion
+ echo "# Enable bash completion" >> ~/.bashrc
+ echo "if [ -f /usr/share/bash-completion/bash_completion ]; then" >> ~/.bashrc
+ echo " . /usr/share/bash-completion/bash_completion" >> ~/.bashrc
+ echo "fi" >> ~/.bashrc
+elif [ -f /etc/bash_completion ]; then
+ . /etc/bash_completion
+ echo "# Enable bash completion" >> ~/.bashrc
+ echo "if [ -f /etc/bash_completion ]; then" >> ~/.bashrc
+ echo " . /etc/bash_completion" >> ~/.bashrc
+ echo "fi" >> ~/.bashrc
+fi
+
echo -e "[*] \033[1mWelcome to robonix docker environment!\033[0m Distro is: \033[33m$(lsb_release -ds 2>/dev/null || echo "Linux")\033[0m with ROS2 \033[33m$(echo $ROS_DISTRO)\033[0m"
exec bash
diff --git a/docs/Makefile b/docs/Makefile
deleted file mode 100644
index d0c3cbf..0000000
--- a/docs/Makefile
+++ /dev/null
@@ -1,20 +0,0 @@
-# Minimal makefile for Sphinx documentation
-#
-
-# You can set these variables from the command line, and also
-# from the environment for the first two.
-SPHINXOPTS ?=
-SPHINXBUILD ?= sphinx-build
-SOURCEDIR = source
-BUILDDIR = build
-
-# Put it first so that "make" without argument is like "make help".
-help:
- @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
-
-.PHONY: help Makefile
-
-# Catch-all target: route all unknown targets to Sphinx using the new
-# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
-%: Makefile
- @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/docs/README.md b/docs/README.md
deleted file mode 100644
index 1af0441..0000000
--- a/docs/README.md
+++ /dev/null
@@ -1,49 +0,0 @@
-# robonix 文档系统
-
-wheatfox
-
-本目录包含 robonix 项目的完整技术文档,使用 Sphinx 构建系统生成。
-
-## 构建文档
-
-### 安装依赖
-
-```bash
-pip install sphinx sphinxawesome-theme myst-parser
-```
-
-### 构建 HTML 文档
-
-```bash
-cd docs
-make html
-```
-
-构建完成后,HTML 文档位于 `build/html/index.html`。
-
-### 其他格式
-
-```bash
-# 构建 PDF 文档(需要 LaTeX)
-make latexpdf
-
-# 构建 EPUB 文档
-make epub
-
-# 清理构建文件
-make clean
-```
-
-## 维护说明
-
-- 文档源文件使用 reStructuredText 格式
-- 配置文件 `conf.py` 包含了主题、扩展和路径设置
-- API 文档通过 `autodoc` 扩展自动生成
-- 支持 Markdown 文件(通过 `myst-parser` 扩展)
-
-## 贡献指南
-
-1. 修改 `source/` 目录下的 `.rst` 文件
-2. 运行 `make html` 验证构建结果
-3. 确保文档内容准确、清晰、专业
-4. 遵循现有的文档结构和风格约定
\ No newline at end of file
diff --git a/docs/deploy.sh b/docs/deploy.sh
deleted file mode 100755
index fd2bbf2..0000000
--- a/docs/deploy.sh
+++ /dev/null
@@ -1,288 +0,0 @@
-#!/bin/bash
-
-# robonix Documentation Deployment Script
-# This script copies built HTML documentation to the remote server
-
-set -e # Exit on any error
-
-# Configuration
-BUILD_DIR="build/html" # HTML documentation directory
-REMOTE_USER="root" # Server username
-REMOTE_HOST="47.94.74.133" # Your server IP address
-REMOTE_PATH="/www/wwwroot/docs.oscommunity.cn"
-SSH_CONTROL_PATH="/tmp/ssh_deploy_$$" # SSH connection multiplexing control path
-
-# Colors for output
-RED='\033[0;31m'
-GREEN='\033[0;32m'
-YELLOW='\033[1;33m'
-BLUE='\033[0;34m'
-NC='\033[0m' # No Color
-
-# Function to print colored output
-print_status() {
- echo -e "${BLUE}[INFO]${NC} $1"
-}
-
-print_success() {
- echo -e "${GREEN}[SUCCESS]${NC} $1"
-}
-
-print_warning() {
- echo -e "${YELLOW}[WARNING]${NC} $1"
-}
-
-print_error() {
- echo -e "${RED}[ERROR]${NC} $1"
-}
-
-# Check if build directory exists
-check_build_dir() {
- if [ ! -d "$BUILD_DIR" ]; then
- print_error "Build directory '$BUILD_DIR' not found!"
- print_warning "Please run the documentation build process first."
- exit 1
- fi
-
- if [ ! "$(ls -A $BUILD_DIR)" ]; then
- print_error "Build directory '$BUILD_DIR' is empty!"
- print_warning "Please run the documentation build process first."
- exit 1
- fi
-
- print_success "Build directory found and contains files."
-}
-
-# Setup SSH connection multiplexing
-setup_ssh_connection() {
- print_status "Setting up SSH connection to $REMOTE_USER@$REMOTE_HOST..."
- print_warning "Please enter the root password (you'll only need to enter it once):"
-
- # Create SSH master connection with multiplexing
- ssh -M -S "$SSH_CONTROL_PATH" -f -N -o ConnectTimeout=10 "$REMOTE_USER@$REMOTE_HOST"
-
- if [ $? -eq 0 ]; then
- print_success "SSH connection established and ready for reuse."
- else
- print_error "Failed to establish SSH connection to $REMOTE_USER@$REMOTE_HOST"
- print_warning "Please check:"
- echo " - Server IP/hostname: $REMOTE_HOST"
- echo " - Username: $REMOTE_USER"
- echo " - Password correctness"
- echo " - Network connectivity"
- exit 1
- fi
-}
-
-# Function to run SSH commands using the multiplexed connection
-ssh_run() {
- ssh -S "$SSH_CONTROL_PATH" "$REMOTE_USER@$REMOTE_HOST" "$@"
-}
-
-# Function to run SCP using the multiplexed connection
-scp_run() {
- scp -o "ControlPath=$SSH_CONTROL_PATH" "$@"
-}
-
-# Function to run rsync using the multiplexed connection
-rsync_run() {
- rsync -e "ssh -S $SSH_CONTROL_PATH" "$@"
-}
-
-# Cleanup SSH connection
-cleanup_ssh_connection() {
- if [ -S "$SSH_CONTROL_PATH" ]; then
- print_status "Closing SSH connection..."
- ssh -S "$SSH_CONTROL_PATH" -O exit "$REMOTE_USER@$REMOTE_HOST" 2>/dev/null || true
- rm -f "$SSH_CONTROL_PATH"
- fi
-}
-
-# Create remote directory if it doesn't exist
-create_remote_dir() {
- print_status "Ensuring remote directory exists..."
- ssh_run "mkdir -p $REMOTE_PATH"
- print_success "Remote directory ready."
-}
-
-# Backup existing documentation (optional)
-backup_existing_docs() {
- print_status "Creating backup of existing documentation..."
- BACKUP_NAME="docs_backup_$(date +%Y%m%d_%H%M%S)"
- ssh_run "
- if [ -d '$REMOTE_PATH' ] && [ \"\$(ls -A $REMOTE_PATH 2>/dev/null)\" ]; then
- cp -r $REMOTE_PATH ${REMOTE_PATH}_$BACKUP_NAME
- echo 'Backup created: ${REMOTE_PATH}_$BACKUP_NAME'
- else
- echo 'No existing documentation to backup.'
- fi
- "
-}
-
-# Deploy documentation
-deploy_docs() {
- print_status "Deploying documentation to $REMOTE_HOST:$REMOTE_PATH..."
-
- # Show what will be deployed
- print_status "Files to be deployed:"
- find "$BUILD_DIR" -type f | head -10
- if [ $(find "$BUILD_DIR" -type f | wc -l) -gt 10 ]; then
- print_status "... and $(( $(find "$BUILD_DIR" -type f | wc -l) - 10 )) more files"
- fi
-
- # Check if rsync is available on both local and remote systems
- if command -v rsync >/dev/null 2>&1; then
- print_status "Checking if rsync is available on remote server..."
- if ssh_run "command -v rsync >/dev/null 2>&1"; then
- print_status "Using rsync for efficient transfer..."
- print_status "Copying all files from $BUILD_DIR/ to $REMOTE_PATH/"
- rsync_run -avz --progress --delete \
- "$BUILD_DIR/" \
- "$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/"
- else
- print_warning "rsync not found on remote server, falling back to scp..."
- print_status "Using scp for file transfer..."
- print_status "Copying all files from $BUILD_DIR/* to $REMOTE_PATH/"
- scp_run -r "$BUILD_DIR"/* "$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/"
- fi
- else
- print_status "Using scp for file transfer..."
- print_status "Copying all files from $BUILD_DIR/* to $REMOTE_PATH/"
- scp_run -r "$BUILD_DIR"/* "$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/"
- fi
-
- # Verify deployment
- print_status "Verifying deployment..."
- REMOTE_FILE_COUNT=$(ssh_run "find $REMOTE_PATH -type f | wc -l")
- LOCAL_FILE_COUNT=$(find "$BUILD_DIR" -type f | wc -l)
- print_status "Local files: $LOCAL_FILE_COUNT, Remote files: $REMOTE_FILE_COUNT"
-
- if [ "$REMOTE_FILE_COUNT" -eq "$LOCAL_FILE_COUNT" ]; then
- print_success "All files deployed successfully!"
- else
- print_warning "File count mismatch. Local: $LOCAL_FILE_COUNT, Remote: $REMOTE_FILE_COUNT"
- print_status "This might be normal if there are hidden files or different file types."
- fi
-
- print_success "Documentation deployed successfully!"
-}
-
-# Set proper permissions on remote server
-set_permissions() {
- print_status "Setting proper permissions on remote server..."
- ssh_run "
- # Set ownership (ignore errors for system files)
- chown -R www-data:www-data $REMOTE_PATH 2>/dev/null || chown -R nginx:nginx $REMOTE_PATH 2>/dev/null || true
-
- # Set permissions, but skip files with special attributes like .user.ini
- find $REMOTE_PATH -type f -name '*.user.ini' -exec echo 'Skipping protected file: {}' \; 2>/dev/null || true
- find $REMOTE_PATH -type f ! -name '*.user.ini' -exec chmod 644 {} \; 2>/dev/null || true
- find $REMOTE_PATH -type d -exec chmod 755 {} \; 2>/dev/null || true
-
- # Try to set general permissions, but don't fail if some files can't be changed
- chmod -R 755 $REMOTE_PATH 2>/dev/null || {
- echo 'Some files could not have permissions changed (this is normal for protected files like .user.ini)'
- }
- "
- print_success "Permissions set (protected files skipped)."
-}
-
-# Main deployment function
-main() {
- print_status "Starting robonix documentation deployment..."
- echo "=================================="
-
- # Setup trap to cleanup SSH connection on exit
- trap cleanup_ssh_connection EXIT
-
- # Pre-deployment checks
- check_build_dir
- setup_ssh_connection
-
- # Create remote directory
- create_remote_dir
-
- # Optional: Create backup
- read -p "Do you want to backup existing documentation? (y/N): " -n 1 -r
- echo
- if [[ $REPLY =~ ^[Yy]$ ]]; then
- backup_existing_docs
- fi
-
- # Deploy
- deploy_docs
-
- # Set permissions
- set_permissions
-
- echo "=================================="
- print_success "Deployment completed successfully!"
- print_status "Documentation is now available at: http://docs.oscommunity.cn"
-
- # Cleanup SSH connection
- cleanup_ssh_connection
-}
-
-# Help function
-show_help() {
- echo "robonix Documentation Deployment Script"
- echo ""
- echo "Usage: $0 [OPTIONS]"
- echo ""
- echo "OPTIONS:"
- echo " -h, --help Show this help message"
- echo " --no-backup Skip backup creation"
- echo " --dry-run Show what would be deployed without actually doing it"
- echo ""
- echo "Configuration:"
- echo " BUILD_DIR: $BUILD_DIR"
- echo " REMOTE_USER: $REMOTE_USER"
- echo " REMOTE_HOST: $REMOTE_HOST"
- echo " REMOTE_PATH: $REMOTE_PATH"
- echo ""
- echo "Before running:"
- echo " 1. Build your documentation first"
- echo " 2. Ensure you have the root password for the server"
- echo " 3. Make sure SSH password authentication is enabled on the server"
-}
-
-# Parse command line arguments
-case "${1:-}" in
- -h|--help)
- show_help
- exit 0
- ;;
- --dry-run)
- print_status "DRY RUN: Would deploy from $BUILD_DIR to $REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH"
- check_build_dir
- print_status "Files to be deployed:"
- find "$BUILD_DIR" -type f | head -20
- if [ $(find "$BUILD_DIR" -type f | wc -l) -gt 20 ]; then
- print_status "... and $(( $(find "$BUILD_DIR" -type f | wc -l) - 20 )) more files"
- fi
- exit 0
- ;;
- --no-backup)
- print_status "Backup creation will be skipped."
- # Setup trap to cleanup SSH connection on exit
- trap cleanup_ssh_connection EXIT
- # Run main without backup prompt
- check_build_dir
- setup_ssh_connection
- create_remote_dir
- deploy_docs
- set_permissions
- print_success "Deployment completed successfully!"
- cleanup_ssh_connection
- exit 0
- ;;
- "")
- # No arguments, run normally
- main
- ;;
- *)
- print_error "Unknown option: $1"
- show_help
- exit 1
- ;;
-esac
diff --git a/docs/robonix.png b/docs/robonix.png
deleted file mode 100644
index b362f01..0000000
Binary files a/docs/robonix.png and /dev/null differ
diff --git a/docs/source/_static/custom.css b/docs/source/_static/custom.css
deleted file mode 100644
index 3624a36..0000000
--- a/docs/source/_static/custom.css
+++ /dev/null
@@ -1,43 +0,0 @@
-/* 隐藏左侧导航栏的滚动条,但保持滚动功能 */
-.bd-sidebar {
- overflow-y: auto;
- scrollbar-width: none; /* Firefox */
- -ms-overflow-style: none; /* Internet Explorer 10+ */
-}
-
-.bd-sidebar::-webkit-scrollbar {
- display: none; /* WebKit */
-}
-
-/* 鼠标悬停时显示滚动条 */
-.bd-sidebar:hover {
- scrollbar-width: thin; /* Firefox */
- -ms-overflow-style: auto; /* Internet Explorer 10+ */
-}
-
-.bd-sidebar:hover::-webkit-scrollbar {
- display: block; /* WebKit */
- width: 6px;
-}
-
-.bd-sidebar:hover::-webkit-scrollbar-track {
- background: transparent;
-}
-
-.bd-sidebar:hover::-webkit-scrollbar-thumb {
- background-color: rgba(0, 0, 0, 0.3);
- border-radius: 3px;
-}
-
-.bd-sidebar:hover::-webkit-scrollbar-thumb:hover {
- background-color: rgba(0, 0, 0, 0.5);
-}
-
-/* 优化导航栏的整体样式 */
-.bd-sidebar-primary {
- max-height: calc(100vh - 4rem);
-}
-
-.bd-sidebar .bd-toc {
- max-height: calc(100vh - 8rem);
-}
diff --git a/docs/source/_static/uapi_framework.png b/docs/source/_static/uapi_framework.png
deleted file mode 100644
index 248af57..0000000
Binary files a/docs/source/_static/uapi_framework.png and /dev/null differ
diff --git a/docs/source/api/capability.rst b/docs/source/api/capability.rst
deleted file mode 100644
index 80b2d86..0000000
--- a/docs/source/api/capability.rst
+++ /dev/null
@@ -1,49 +0,0 @@
-Capability Module
-===============================
-
-本章提供Capability模块各个组件的详细API参考文档。
-
-capability.example_hello Module
--------------------------------
-
-.. automodule:: capability.example_hello.api.hello_api
- :members:
- :undoc-members:
- :show-inheritance:
- :ignore-module-all:
-
-capability.example_hello.src Module
------------------------------------
-
-.. automodule:: capability.example_hello.src.hello_src
- :members:
- :undoc-members:
- :show-inheritance:
- :ignore-module-all:
-
-capability.navigation2.api Module
----------------------------------
-
-.. automodule:: capability.navigation2.api.api
- :members:
- :undoc-members:
- :show-inheritance:
- :ignore-module-all:
-
-capability.sim_vision.api Module
---------------------------------
-
-.. automodule:: capability.sim_vision.api.api
- :members:
- :undoc-members:
- :show-inheritance:
- :ignore-module-all:
-
-capability.vision.api Module
-----------------------------
-
-.. automodule:: capability.vision.api.api
- :members:
- :undoc-members:
- :show-inheritance:
- :ignore-module-all:
diff --git a/docs/source/api/driver.rst b/docs/source/api/driver.rst
deleted file mode 100644
index 4b2ec3a..0000000
--- a/docs/source/api/driver.rst
+++ /dev/null
@@ -1,31 +0,0 @@
-Driver Module
-===========================
-
-本章提供Driver模块各个组件的详细API参考文档。
-
-driver.raspi_bm01 Module
-------------------------
-
-.. automodule:: driver.raspi_bm01.src.driver
- :members:
- :undoc-members:
- :show-inheritance:
- :ignore-module-all:
-
-driver.raspi_hc_sr04 Module
----------------------------
-
-.. automodule:: driver.raspi_hc_sr04.src.raspi_hc_sr04.driver
- :members:
- :undoc-members:
- :show-inheritance:
- :ignore-module-all:
-
-driver.sim_genesis_ranger Module
---------------------------------
-
-.. automodule:: driver.sim_genesis_ranger.driver
- :members:
- :undoc-members:
- :show-inheritance:
- :ignore-module-all:
diff --git a/docs/source/api/genesis.rst b/docs/source/api/genesis.rst
deleted file mode 100644
index 8a05b53..0000000
--- a/docs/source/api/genesis.rst
+++ /dev/null
@@ -1,52 +0,0 @@
-Simulator Module (Genesis)
-==================================
-
-本章提供本项目基于Genesis模拟器开发的各个模块的详细API参考文档。
-
-simulator.genesis.scene_manager Module
---------------------------------------
-
-.. automodule:: simulator.genesis.scene_manager
- :members:
- :undoc-members:
- :show-inheritance:
-
-simulator.genesis.car_controller Module
----------------------------------------
-
-.. automodule:: simulator.genesis.car_controller
- :members:
- :undoc-members:
- :show-inheritance:
-
-simulator.genesis.camera_manager Module
----------------------------------------
-
-.. automodule:: simulator.genesis.camera_manager
- :members:
- :undoc-members:
- :show-inheritance:
-
-simulator.genesis.grpc_service Module
--------------------------------------
-
-.. automodule:: simulator.genesis.grpc_service
- :members:
- :undoc-members:
- :show-inheritance:
-
-simulator.genesis.main_loop Module
-----------------------------------
-
-.. automodule:: simulator.genesis.main_loop
- :members:
- :undoc-members:
- :show-inheritance:
-
-simulator.genesis.keyboard_device Module
-----------------------------------------
-
-.. automodule:: simulator.genesis.keyboard_device
- :members:
- :undoc-members:
- :show-inheritance:
diff --git a/docs/source/api/index.rst b/docs/source/api/index.rst
deleted file mode 100644
index 67ace2a..0000000
--- a/docs/source/api/index.rst
+++ /dev/null
@@ -1,51 +0,0 @@
-API 参考文档
-=============
-
-本章节提供Robonix OS系统的完整API参考文档。
-
-.. toctree::
- :maxdepth: 2
- :caption: API 文档
-
- skill_specs_table
- uapi
- genesis
- skill
- manager
- driver
- capability
-
-技能规范
---------
-
-:doc:`skill_specs_table` 提供了系统中所有可用技能和能力的详细规范。
-
-UAPI 模块
----------
-
-:doc:`uapi` 提供了用户API模块的详细文档。
-
-Genesis 模拟器
---------------
-
-:doc:`genesis` 提供了基于Genesis模拟器开发的各个模块的API参考。
-
-Driver 模块
-------------
-
-:doc:`driver` 提供了Driver模块的详细文档。
-
-Manager 模块
-------------
-
-:doc:`manager` 提供了Manager模块的详细文档。
-
-Capability 实现模块
--------------------
-
-:doc:`capability` 提供了Capability实现模块的详细文档。
-
-Skill 实现模块
---------------
-
-:doc:`skill` 提供了Skill实现模块的详细文档。
diff --git a/docs/source/api/manager.rst b/docs/source/api/manager.rst
deleted file mode 100644
index e164d90..0000000
--- a/docs/source/api/manager.rst
+++ /dev/null
@@ -1,78 +0,0 @@
-Manager Module
-============================
-
-本章提供Manager模块各个组件的详细API参考文档。
-
-BaseNode Class
---------------
-
-.. autoclass:: manager.node.BaseNode
- :members:
- :undoc-members:
- :show-inheritance:
-
-ProcessNode Class
------------------
-
-.. autoclass:: manager.process_manage.ProcessNode
- :members:
- :undoc-members:
- :show-inheritance:
-
-RuntimeManager Class
---------------------
-
-.. autoclass:: manager.process_manage.RuntimeManager
- :members:
- :undoc-members:
- :show-inheritance:
-
-eaios Decorator Class
----------------------
-
-.. autoclass:: manager.eaios_decorators.eaios
- :members:
- :undoc-members:
- :show-inheritance:
-
-FunctionRegistry Class
-----------------------
-
-.. autoclass:: manager.eaios_decorators.FunctionRegistry
- :members:
- :undoc-members:
- :show-inheritance:
-
-Command Class
--------------
-
-.. autoclass:: manager.cmdline.Command
- :members:
- :undoc-members:
- :show-inheritance:
-
-CommandRegistry Class
----------------------
-
-.. autoclass:: manager.cmdline.CommandRegistry
- :members:
- :undoc-members:
- :show-inheritance:
-
-Manager Constants
------------------
-
-.. automodule:: manager.constant
- :members:
- :undoc-members:
- :show-inheritance:
- :ignore-module-all:
-
-Manager Logging
----------------
-
-.. automodule:: manager.log
- :members:
- :undoc-members:
- :show-inheritance:
- :ignore-module-all:
diff --git a/docs/source/api/skill.rst b/docs/source/api/skill.rst
deleted file mode 100644
index 1de0583..0000000
--- a/docs/source/api/skill.rst
+++ /dev/null
@@ -1,49 +0,0 @@
-Skill Module
-==========================
-
-本章提供Skill模块各个组件的详细API参考文档。
-
-skill.move.api Module
----------------------
-
-.. automodule:: skill.move.api.api
- :members:
- :undoc-members:
- :show-inheritance:
- :ignore-module-all:
-
-skill.vision.api Module
------------------------
-
-.. automodule:: skill.vision.api.api
- :members:
- :undoc-members:
- :show-inheritance:
- :ignore-module-all:
-
-skill.semantic_map.api Module
------------------------------
-
-.. automodule:: skill.semantic_map.api.api
- :members:
- :undoc-members:
- :show-inheritance:
- :ignore-module-all:
-
-skill.sim_vision.api Module
----------------------------
-
-.. automodule:: skill.sim_vision.api.api
- :members:
- :undoc-members:
- :show-inheritance:
- :ignore-module-all:
-
-skill.test_skill.api Module
----------------------------
-
-.. automodule:: skill.test_skill.api.api
- :members:
- :undoc-members:
- :show-inheritance:
- :ignore-module-all:
diff --git a/docs/source/api/skill_specs_table.rst b/docs/source/api/skill_specs_table.rst
deleted file mode 100644
index 2343ad2..0000000
--- a/docs/source/api/skill_specs_table.rst
+++ /dev/null
@@ -1,130 +0,0 @@
-.. _skill_specs_table:
-
-Capability/Skill 调用规范表
-==============================================
-
-版本:2025年9月2日
-
-本文档展示了Robonix OS系统中所有可用的能力(Capabilities)和技能(Skills)的完整规范。
-
-技能规范表格
-------------
-
-能力 (Capabilities)
-~~~~~~~~~~~~~~~~~~~
-
-.. list-table:: 系统能力列表
- :header-rows: 1
- :widths: 20 50 15 15
-
- * - 能力名称
- - 描述
- - 输入类型
- - 输出类型
- * - cap_camera_rgb
- - 从指定摄像头获取RGB图像
- - camera_name: str, timeout_sec: float
- - opencv图像 (numpy array)
- * - cap_camera_dep_rgb
- - 从指定摄像头获取RGB和深度图像
- - camera_name: str, timeout_sec: float
- - Tuple[rgb_image, depth_image]
- * - cap_camera_info
- - 获取指定摄像头的参数信息
- - camera_name: str, timeout_sec: float
- - Dict[str, Any]
- * - cap_save_rgb_image
- - 捕获并保存RGB图像到文件
- - filename: str, camera_name: str, width: int, height: int
- - success: bool
- * - cap_save_depth_image
- - 捕获并保存深度图像到文件
- - filename: str, camera_name: str, width: int, height: int
- - success: bool
- * - cap_get_robot_pose
- - 获取机器人当前位姿
- - timeout_sec: float
- - x: float, y: float, z: float, yaw: float
- * - cap_set_goal
- - 设置机器人目标点
- - x: float, y: float, yaw: float
- - str
- * - cap_stop_goal
- - 停止机器人目标点
- - None
- - str
- * - cap_get_object_global_pos
- - 基于机器人位姿、像素坐标、深度和相机参数计算物体全局位置
- - pixel_x: float, pixel_y: float, depth: float, camera_info: Dict, robot_pose: Dict
- - Tuple[float, float, float]
- * - cap_get_pose
- - 获取机器人当前位姿
- - None 或 timeout_sec: float
- - Tuple[float, float, float]
- * - cap_tf_transform
- - 坐标系变换
- - source_frame: str, target_frame: str, x: float, y: float, z: float
- - Tuple[float, float, float]
-
-技能 (Skills)
-~~~~~~~~~~~~~~
-
-.. list-table:: 系统技能列表
- :header-rows: 1
- :widths: 20 40 20 20
-
- * - 技能名称
- - 描述
- - 输入类型
- - 依赖能力
- * - skl_debug_test_skill
- - 测试技能
- - input_val: int
- - 无
- * - skl_detect_objs
- - 在指定摄像头的当前视野中检测物体
- - camera_name: str
- - cap_camera_dep_rgb, cap_camera_info, cap_get_object_global_pos, cap_get_robot_pose
- * - skl_move_to_goal
- - 移动机器人到目标点
- - goal_name: str
- - cap_set_goal
- * - skl_move_to_ab_pos
- - 移动机器人到绝对位置
- - x: float, y: float, yaw: float
- - cap_set_goal
- * - skl_move_to_rel_pos
- - 移动机器人到相对位置
- - dx: float, dy: float, dyaw: float
- - cap_set_goal, cap_get_pos
- * - skl_update_map
- - 更新语义地图
- - camera_name: str
- - skl_detect_objs
-
-已弃用的能力
-~~~~~~~~~~~~
-
-.. list-table:: 已弃用的能力列表
- :header-rows: 1
- :widths: 20 50 15 15
-
- * - 能力名称
- - 描述
- - 输入类型
- - 输出类型
- * - cap_space_getpos
- - 获取实体位置 (已弃用)
- - None
- - x: float, y: float, z: float
- * - cap_space_move
- - 移动实体到指定位置 (已弃用)
- - x: float, y: float, z: float
- - success: bool
-
-技能规范源码
-------------
-
-.. literalinclude:: ../../../uapi/specs/skill_specs.py
- :language: python
- :linenos:
\ No newline at end of file
diff --git a/docs/source/api/uapi.rst b/docs/source/api/uapi.rst
deleted file mode 100644
index 32178ed..0000000
--- a/docs/source/api/uapi.rst
+++ /dev/null
@@ -1,44 +0,0 @@
-UAPI Module
-=========================
-
-本章提供UAPI模块各个组件的详细API参考文档。
-
-uapi.runtime.runtime Module
--------------------------
-
-.. automodule:: uapi.runtime.runtime
- :members:
- :undoc-members:
- :show-inheritance:
-
-uapi.runtime.action Module
-------------------------
-
-.. automodule:: uapi.runtime.action
- :members:
- :undoc-members:
- :show-inheritance:
-
-uapi.graph.entity Module
-----------------------
-
-.. automodule:: uapi.graph.entity
- :members:
- :undoc-members:
- :show-inheritance:
-
-uapi.specs.skill_specs Module
----------------------------
-
-.. automodule:: uapi.specs.skill_specs
- :members:
- :undoc-members:
- :show-inheritance:
-
-uapi.specs.types Module
---------------------
-
-.. automodule:: uapi.specs.types
- :members:
- :undoc-members:
- :show-inheritance:
diff --git a/docs/source/conf.py b/docs/source/conf.py
deleted file mode 100644
index db80dc6..0000000
--- a/docs/source/conf.py
+++ /dev/null
@@ -1,154 +0,0 @@
-# Configuration file for the Sphinx documentation builder.
-#
-# For the full list of built-in configuration values, see the documentation:
-# https://www.sphinx-doc.org/en/master/usage/configuration.html
-
-# -- Project information -----------------------------------------------------
-# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
-
-project = 'robonix OS'
-copyright = '2025, Syswonder'
-author = 'Syswonder'
-
-version = '0.0.1'
-release = '0.0.1'
-
-# -- General configuration ---------------------------------------------------
-# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
-
-extensions = [
- 'sphinx.ext.autodoc',
- 'sphinx.ext.viewcode',
- 'sphinx.ext.napoleon',
- 'myst_parser'
-]
-
-templates_path = ['_templates']
-exclude_patterns = []
-
-language = 'en_US'
-
-# Syntax highlighting
-pygments_style = 'default'
-highlight_language = 'python'
-highlight_options = {'stripnl': False}
-
-# -- Options for HTML output -------------------------------------------------
-# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
-
-html_permalinks_icon = '#'
-html_theme = 'sphinxawesome_theme'
-html_static_path = ['_static']
-html_css_files = [
- 'custom.css',
-]
-
-# Add project root to Python path for autodoc
-import os
-import sys
-sys.path.insert(0, os.path.abspath('../../'))
-
-# Mock imports for dependencies not available during doc build
-autodoc_mock_imports = [
- 'loguru',
- 'pynput',
- 'genesis',
- 'grpc',
- 'numpy',
- 'opencv',
- 'cv2',
- 'scipy',
- 'scipy.spatial',
- 'scipy.spatial.transform',
- 'robot_control_pb2_grpc',
- 'robot_control_pb2',
- 'ultralytics',
- 'ultralytics.models',
- 'ultralytics.models.YOLOE',
- 'ultralytics.models.YOLO',
- 'ultralytics.models.FastSAM',
- 'ultralytics.models.SAM',
- 'ultralytics.models.RTDETR',
- 'ultralytics.models.NAS',
- 'ultralytics.models.YOLOWorld',
- 'ultralytics.engine',
- 'ultralytics.engine.model',
- 'ultralytics.engine.results',
- 'ultralytics.data',
- 'ultralytics.data.augment',
- 'ultralytics.data.base',
- 'ultralytics.data.utils',
- 'ultralytics.utils',
- 'ultralytics.utils.ops',
- 'ultralytics.utils.metrics',
- 'aioconsole',
- 'rclpy',
- 'rclpy.node',
- 'geometry_msgs',
- 'geometry_msgs.msg',
- 'nav2_simple_commander',
- 'nav2_simple_commander.robot_navigator',
- 'sensor_msgs',
- 'sensor_msgs.msg',
- 'std_srvs',
- 'std_srvs.srv',
- 'std_msgs',
- 'std_msgs.msg',
- 'mcp',
- 'mcp.server',
- 'mcp.server.fastmcp',
- 'RPi',
- 'RPi.GPIO',
- 'yaml',
- 'threading',
- 'time',
- 'signal',
- 'logging',
- 'argparse',
- 'asyncio',
- 'atexit',
- 'math',
- 'random',
- 'datetime',
- 'traceback',
- 'typing',
- 'typing.Dict',
- 'typing.Optional',
- 'typing.List',
- 'typing.Callable',
- 'typing.Tuple',
- 'typing.Union'
-]
-
-# Napoleon settings
-napoleon_google_docstring = True
-napoleon_numpy_docstring = True
-napoleon_include_init_with_doc = False
-napoleon_include_private_with_doc = False
-
-# Autodoc settings
-autodoc_default_options = {
- 'members': True,
- 'member-order': 'bysource',
- 'special-members': '__init__',
- 'undoc-members': True,
- 'exclude-members': '__weakref__'
-}
-
-# Suppress warnings for missing imports
-suppress_warnings = ['autodoc.import_object']
-
-# Source file settings
-source_suffix = {
- '.rst': None,
-}
-
-# HTML theme options for sphinxawesome_theme
-html_theme_options = {
- 'show_breadcrumbs': True,
- 'breadcrumbs_separator': ' / ',
- 'main_nav_links': {
- 'Docs': 'index',
- 'API': 'api/index',
- },
-}
diff --git a/docs/source/examples/simple_demo.rst b/docs/source/examples/simple_demo.rst
deleted file mode 100644
index 7b845e4..0000000
--- a/docs/source/examples/simple_demo.rst
+++ /dev/null
@@ -1,130 +0,0 @@
-Simple Demo (Genesis)
-============
-
-本章通过 ``examples/demo1`` 中的完整示例,演示如何使用Robonix OS框架开发具身应用应用。
-
-环境配置
--------
-
-运行示例前,需要完成以下环境配置步骤:
-
-**系统要求**
-
-- Python 3.12
-- NVIDIA 显卡驱动和 CUDA 工具包
-- OpenGL 支持
-
-**安装系统依赖**
-
-.. code-block:: bash
-
- # Install OpenGL related dependencies
- sudo apt-get install freeglut3 freeglut3-dev mesa-utils
-
- # Configure NVIDIA as OpenGL rendering backend (Important!)
- export __GLX_VENDOR_LIBRARY_NAME=nvidia
-
-**创建 Python 环境**
-
-.. code-block:: bash
-
- # Create and activate conda environment
- conda create -n genesis python=3.12
- conda activate genesis
-
-**安装依赖包**
-
-.. code-block:: bash
-
- # First install PyTorch according to https://pytorch.org/get-started/locally/
- # For example: pip3 install torch torchvision (Linux CUDA 12.8)
-
- # Install other dependencies
- pip install rich loguru mcp pyyaml argparse grpcio grpcio-tools ultraimport \
- ultralytics genesis-world pynput openai python-dotenv opencv-python
-
-Genesis 模拟器启动
------------------
-
-**1. 配置模拟器连接**
-
-在运行示例前,需要修改模拟器连接配置。编辑 ``driver/sim_genesis_ranger/driver.py`` 文件,将 ``TARGET_SERVER_IP`` 修改为模拟器运行的 IP 地址:
-
-- 本地运行:使用 ``localhost`` 或 ``127.0.0.1``
-- 远程服务器:使用服务器的实际 IP 地址
-
-**2. 启动模拟器**
-
-.. code-block:: bash
-
- # Start Genesis simulator in robonix root directory
- python start_genesis.py
-
-等待模拟器启动完成,直到出现渲染窗口。
-
-**3. 导出技能系统**
-
-.. code-block:: bash
-
- # Export skill system configuration in robonix root directory (simulator mode)
- python manager/eaios_decorators.py --config config/include/simulator.yml
-
-此命令会生成 ``skill/__init__.py`` 文件,用于技能系统的初始化。
-
-.. note::
- 如果要在物理小车上运行,请使用 ``config/include/ranger_test.yml`` 配置文件。
-
-**4. 下载视觉模型**
-
-为了使用视觉相关技能,需要下载 YOLO 模型:
-
-.. code-block:: bash
-
- # Execute in robonix root directory
- mkdir -p skill/sim_vision/models
- wget -P skill/sim_vision/models https://github.com/ultralytics/assets/releases/download/v8.3.0/yoloe-11l-seg-pf.pt
-
-
-示例概述
--------
-
-``simple_demo.py`` 展示了一个完整的具身应用应用开发流程,包括系统初始化、实体图构建、技能绑定和动作执行。该示例支持两种运行模式,适合不同的使用场景。
-
-运行示例
--------
-
-完成环境配置和模拟器启动后,可以运行示例程序。
-
-其中加载的 action 程序为 ``examples/demo1/simple.action``。
-
-**手动模式**
-
-.. code-block:: bash
-
- # Run in robonix root directory
- python examples/demo1/simple_demo.py --mode manual
-
-手动模式下,用户需要手动指定目标物体和动作参数。
-
-**自动模式**
-
-.. code-block:: bash
-
- # Run in robonix root directory
- python examples/demo1/simple_demo.py --mode auto
-
-自动模式下,系统会:
-
-- 使用 YOLO 模型自动识别场景中的物体
-- 自动生成实体图
-- 自动绑定动作参数
-- 让小车自动移动到识别到的物体位置
-
-**导出场景信息**
-
-.. code-block:: bash
-
- # Run in robonix root directory
- python examples/demo1/simple_demo.py --mode manual --export-scene scene_info.json
-
-此命令可以将当前场景信息导出为 JSON 文件,便于后续分析和调试。
diff --git a/docs/source/genesis/communication_service.rst b/docs/source/genesis/communication_service.rst
deleted file mode 100644
index 942165b..0000000
--- a/docs/source/genesis/communication_service.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-CommunicationService 通信服务
-============
-
-robonix OS 的模拟器相关 driver 通过 Google gRPC 与 Genesis 场景进行通信和控制,proto 文件如下:
-
-.. literalinclude:: ../../../simulator/genesis/robot_control.proto
- :language: protobuf
diff --git a/docs/source/genesis/index.rst b/docs/source/genesis/index.rst
deleted file mode 100644
index 8208dc9..0000000
--- a/docs/source/genesis/index.rst
+++ /dev/null
@@ -1,35 +0,0 @@
-robonix OS 模拟器(基于 Genesis)
-==================
-
-Genesis 模拟器系统是 robonix 框架中的物理仿真组件,基于Genesis物理引擎构建了完整的机器人仿真环境。该系统采用模块化设计,通过场景管理、运动控制、相机系统和远程通信四个核心模块,提供了能够运行用户编写的具身应用程序的机器人仿真平台。
-
-模拟器架构
------------
-
-Genesis模拟器系统采用分层模块化架构,每个模块负责特定的功能领域:
-
-**场景管理(SceneManager)**
- 负责3D仿真环境的创建和管理,包括物理场景初始化、实体添加、材质配置和渲染设置。该层封装了Genesis引擎的底层API,提供了简化的场景构建接口。
-
-**运动控制(CarController)**
- 实现机器人的运动学控制,支持键盘输入处理、速度控制、位置导航和姿态管理。采用平滑的加速度控制模型,确保机器人运动的真实性和稳定性。
-
-**相机管理(CameraManager)**
- 管理仿真环境中的相机系统,提供RGB图像和深度图像的采集、处理和保存功能。支持相机的动态定位和多种图像格式输出。
-
-**通信服务(gRPC Service)**
- 通过gRPC协议提供远程控制接口,支持网络化的机器人控制和状态查询。实现了标准化的机器人控制协议,便于与外部系统集成。
-
-**主程序协调(RobotSimulator)**
- 统筹各个模块的初始化和运行,提供统一的生命周期管理和异常处理机制。
-
-模拟器组件
------------
-
-.. toctree::
- :maxdepth: 2
-
- scene_management
- motion_control
- vision_system
- communication_service
diff --git a/docs/source/genesis/motion_control.rst b/docs/source/genesis/motion_control.rst
deleted file mode 100644
index 5723f19..0000000
--- a/docs/source/genesis/motion_control.rst
+++ /dev/null
@@ -1,195 +0,0 @@
-CarController 运动控制
-============
-
-CarController 是 Genesis 模拟器的运动控制模块,实现了基于物理的运动控制模型,支持键盘输入处理、平滑速度控制、精确位置导航和实时姿态管理,为机器人提供了真实可信的运动行为。
-
-控制架构设计
------------
-
-CarController 类
-~~~~~~~~~~~~~~~~
-
-CarController类采用分层控制架构,将运动控制分解为多个相互协作的子系统:
-
-**输入处理层**
- 负责处理来自键盘、程序接口和网络命令的控制输入。该层实现了输入的标准化和优先级管理,确保不同控制源的协调工作。
-
-**运动规划层**
- 基于当前状态和目标状态,计算最优的运动轨迹。采用平滑的加速度控制模型,避免突兀的速度变化,提高运动的真实性。
-
-**物理执行层**
- 将运动规划的结果转换为Genesis引擎可执行的物理命令,包括位置更新、姿态调整和碰撞处理。
-
-**状态管理层**
- 维护机器人的完整运动状态,包括位置、速度、加速度和姿态信息。提供状态查询和历史记录功能。
-
-运动学模型
----------
-
-**坐标系统**
- 系统采用右手坐标系,X轴向前,Y轴向左,Z轴向上。机器人的运动在XY平面内进行,Z轴方向的旋转表示机器人的朝向角度。
-
-**速度控制模型**
- 机器人的运动通过三个速度分量控制:
-
- - ``vx`` - 前进/后退速度(X轴方向)
- - ``vy`` - 左移/右移速度(Y轴方向)
- - ``wz`` - 旋转角速度(绕Z轴)
-
-**加速度平滑机制**
- 为了避免机器人运动的突兀变化,系统实现了平滑的加速度控制:
-
-.. code-block:: python
-
- def smooth_approach(current, target, accel, dt):
- diff = target - current
- max_change = accel * dt
- if abs(diff) <= max_change:
- return target
- else:
- return current + np.sign(diff) * max_change
-
-这种机制确保了机器人运动的连续性和真实感。
-
-键盘控制
------------
-
-**按键映射配置**
- 系统定义了标准的键盘控制映射:
-
- - 方向键:前进/后退/左移/右移
- - ``[`` / ``]``:左转/右转
- - ``-``:重置到初始位置
- - ``ESC``:退出程序
-
-**实时输入处理**
- 键盘输入通过独立线程进行监听和处理,确保了控制的实时响应性。系统支持多键同时按下,实现复合运动控制。
-
-**优先级管理**
- 当同时存在键盘输入和程序控制时,键盘输入具有更高优先级,这种设计便于调试和紧急干预。
-
-位置导航
------------
-
-MoveTo 功能
-~~~~~~~~~~~~~~
-
-位置导航系统实现了精确的点到点导航能力:
-
-**目标设定机制**
- 通过设置机器人的 ``_move_to_target`` 属性来启动导航任务。目标包含坐标位置和激活状态信息。
-
-**路径规划算法**
- 采用简单而有效的直线路径规划:
-
-.. code-block:: python
-
- # Calculate vector to target
- rem_x = target_x - curr_x
- rem_y = target_y - curr_y
- rem_dist = np.hypot(rem_x, rem_y)
-
- # Normalize direction vector
- if rem_dist > distance_threshold:
- norm_x = rem_x / rem_dist
- norm_y = rem_y / rem_dist
-
- # Calculate target velocity
- self.move_to_vx = norm_x * self.max_speed * 0.8
- self.move_to_vy = norm_y * self.max_speed * 0.8
-
-**到达判定机制**
- 系统使用距离阈值(默认0.1米)来判定是否到达目标位置。到达目标后自动停止导航任务。
-
-**超时保护机制**
- 导航任务设有超时保护,防止因环境障碍或算法问题导致的无限导航。
-
-姿态管理
------------
-
-**姿态表示方法**
- 机器人的姿态使用欧拉角表示,主要关注绕Z轴的旋转角度(yaw角)。系统维护了机器人的当前朝向和目标朝向。
-
-**姿态更新算法**
- 姿态更新基于角速度积分:
-
-.. code-block:: python
-
- # Update robot orientation
- self.car._my_yaw += self.wz * self.dt
-
- # Normalize angle to [-π, π] range
- while self.car._my_yaw > np.pi:
- self.car._my_yaw -= 2 * np.pi
- while self.car._my_yaw < -np.pi:
- self.car._my_yaw += 2 * np.pi
-
-**坐标变换处理**
- 系统支持本体坐标系和世界坐标系之间的变换,确保运动控制的正确性:
-
-.. code-block:: python
-
- # Transform from body coordinate system to world coordinate system
- world_vx = body_vx * np.cos(yaw) - body_vy * np.sin(yaw)
- world_vy = body_vx * np.sin(yaw) + body_vy * np.cos(yaw)
-
-物理集成
------------
-
-**Genesis引擎接口**
- 运动控制系统通过标准接口与Genesis物理引擎集成:
-
- - ``get_qpos()``:获取机器人当前位置和姿态
- - ``set_qpos()``:设置机器人位置和姿态
- - ``get_qvel()``:获取机器人当前速度
- - ``set_qvel()``:设置机器人速度
-
-**物理约束处理**
- 系统考虑了物理世界的约束条件:
-
- - 重力影响:机器人受重力作用,需要地面支撑
- - 碰撞检测:与墙体和障碍物的碰撞会影响运动
- - 摩擦力:地面摩擦影响机器人的加速和减速
-
-**实时同步机制**
- 控制系统与物理仿真保持同步更新,确保控制指令的及时执行和状态反馈的准确性。
-
-状态监控
------------
-
-**状态记录机制**
- 系统持续记录机器人的运动状态,包括:
-
- - 位置历史:记录机器人的运动轨迹
- - 速度历史:监控速度变化趋势
- - 控制输入:记录各种控制命令的执行情况
-
-**性能监控指标**
- - 位置精度:实际位置与目标位置的偏差
- - 响应时间:从控制输入到运动响应的延迟
- - 运动平滑度:速度和加速度的连续性指标
-
-**调试信息输出**
- 系统提供详细的调试信息输出,便于开发者监控和调试运动控制的行为。
-
-重置和恢复
-~~~~~~~~~~~~~
-
-**状态重置功能**
- 系统支持将机器人重置到初始状态:
-
-.. code-block:: python
-
- def reset_car(self):
- # Reset position to initial position
- initial_pos = getattr(self.car, '_initial_pos', (0.0, -2.0, 0.15))
- initial_yaw = getattr(self.car, '_initial_yaw', 0.0)
-
- # Set new position and orientation
- new_qpos = [initial_pos[0], initial_pos[1], initial_pos[2],
- 0.0, 0.0, 0.0, 1.0] # Position + quaternion orientation
- self.car.set_qpos(new_qpos)
-
- # Reset velocity and state
- self.car._my_yaw = initial_yaw
- self.vx = self.vy = self.wz = 0.0
\ No newline at end of file
diff --git a/docs/source/genesis/scene_management.rst b/docs/source/genesis/scene_management.rst
deleted file mode 100644
index 517d660..0000000
--- a/docs/source/genesis/scene_management.rst
+++ /dev/null
@@ -1,109 +0,0 @@
-SceneManager 场景管理
-============
-
-SceneManager 类
-~~~~~~~~~~~~~~~
-
-SceneManager类是场景管理的核心控制器,采用Builder模式设计,通过链式调用构建复杂的仿真场景:
-
-**初始化配置**
- 系统在初始化时配置Genesis引擎的基础参数,包括后端选择(GPU加速)、物理仿真参数和渲染选项。这些配置确保了仿真的性能和质量。
-
-**分步构建流程**
- 场景构建采用分步执行的方式:创建场景 → 添加元素 → 构建场景 → 设置视角。这种设计确保了场景元素的正确初始化和依赖关系的处理。
-
-**资源路径管理**
- 通过 ``_get_sim_asset`` 方法统一管理仿真资源的路径,支持纹理、模型、材质等多种资源类型的加载。
-
-核心功能模块
------------
-
-场景初始化
-~~~~~~~~~
-
-场景初始化是整个仿真环境的基础,包含多个关键配置:
-
-**物理引擎配置**
- - 时间步长设置(dt=0.01)确保仿真精度
- - 重力参数(0, 0, -9.81)模拟真实物理环境
- - GPU后端选择提供高性能计算支持
-
-**视觉渲染配置**
- - 分辨率设置(1366×768)提供高清显示
- - 相机参数配置包括位置、朝向和视野角度
- - 帧率限制(60 FPS)保证流畅的视觉体验
-
-**可视化选项**
- - 坐标系显示控制便于调试和开发
- - 环境光照设置(0.3, 0.3, 0.3)提供合适的光照条件
- - 平面反射效果增强视觉真实感
-
-.. code-block:: python
-
- def create_scene(self):
- gs.init(backend=gs.gpu)
-
- self.scene = gs.Scene(
- show_viewer=True,
- sim_options=gs.options.SimOptions(
- dt=0.01,
- gravity=(0, 0, -9.81),
- ),
- viewer_options=gs.options.ViewerOptions(
- res=(1366, 768),
- camera_pos=(-4.0, 2.5, 3.0),
- camera_lookat=(0.0, 0.0, 0.5),
- camera_fov=45,
- max_FPS=60,
- ),
- renderer=gs.renderers.Rasterizer(),
- )
-
-环境元素构建
-~~~~~~~~~~~
-
-**地面系统**
- 地面采用平面几何体,配置了真实的瓷砖纹理和法线贴图。粗糙度设置为0.9,模拟真实地面的摩擦特性。纹理映射增强了视觉效果的真实感。
-
-**墙体结构**
- 房间采用四面墙体的封闭结构,每面墙都是独立的盒子几何体。墙体设置为固定实体(fixed=True),不参与物理碰撞计算,但提供碰撞边界。
-
-**机器人载体**
- 机器人使用盒子几何体表示,配置了铁质材料和蓝色外观。机器人是可移动实体(fixed=False),参与完整的物理仿真,包括重力、惯性和碰撞响应。
-
-**装饰家具**
- 系统支持加载复杂的3D模型作为场景装饰,如椅子等家具。这些模型使用OBJ格式,配备完整的材质和纹理信息,增强了场景的真实感。
-
-相机系统集成
-~~~~~~~~~~~
-
-**相机参数配置**
- 相机系统支持灵活的参数配置,包括分辨率、位置、朝向和视野角度。这些参数可以根据不同的应用需求进行调整。
-
-**动态定位机制**
- 相机支持相对于机器人的动态定位,可以实现跟随拍摄、固定视角拍摄等多种模式。位置计算考虑了机器人的当前姿态和运动状态。
-
-**多相机支持**
- 系统架构支持多个相机的同时使用,每个相机都有独立的参数配置和数据输出通道。
-
-材质和纹理系统
-~~~~~~~~~~~~~
-
-**纹理映射机制**
- - 漫反射纹理(diffuse_texture):定义物体的基础颜色
- - 法线纹理(normal_texture):增加表面细节和凹凸感
- - 粗糙度纹理(roughness_texture):控制表面的光泽度分布
-
-**资源管理策略**
- 纹理资源通过统一的路径管理系统加载,支持相对路径和绝对路径。系统会自动处理资源的缓存和内存管理。
-
-场景构建流程
------------
-
-**标准构建流程**
- 1. 初始化Genesis引擎和场景对象
- 2. 添加基础环境元素(地面、墙体)
- 3. 添加机器人和相机等动态元素
- 4. 添加装饰性元素(家具、道具)
- 5. 执行场景构建(build)操作
- 6. 配置视角跟随和渲染参数
\ No newline at end of file
diff --git a/docs/source/genesis/vision_system.rst b/docs/source/genesis/vision_system.rst
deleted file mode 100644
index e3c4ffc..0000000
--- a/docs/source/genesis/vision_system.rst
+++ /dev/null
@@ -1,129 +0,0 @@
-CameraManager 相机管理
-========
-
-CameraManager 是 Genesis 模拟器的相机管理模块,提供了完整的机器人视觉仿真能力。该系统通过多线程架构实现了实时图像采集、处理和存储功能,支持RGB图像、深度图像和相机参数的获取,为机器人的视觉算法提供了高质量的数据支持。
-
-系统架构设计
------------
-
-CameraManager 类
-~~~~~~~~~~~~~~~~
-
-CameraManager类采用生产者-消费者模式设计,通过独立线程处理图像采集任务,避免阻塞主仿真循环:
-
-**多线程处理架构**
- - 主线程负责仿真控制和用户交互
- - 相机线程专门处理图像采集和处理
- - 线程间通过事件机制进行同步和通信
-
-**资源管理机制**
- - 自动创建输出目录结构
- - 智能的文件命名和版本管理
- - 完善的线程生命周期管理
-
-**配置参数系统**
- - 灵活的采集间隔设置
- - 可配置的输出路径和格式
- - 动态的图像质量参数调节
-
-核心功能模块
------------
-
-相机定位系统
-~~~~~~~~~~~
-
-**动态跟随机制**
- 相机系统实现了智能的机器人跟随功能,相机位置根据机器人的实时状态动态调整:
-
-.. code-block:: python
-
- # Get robot current state
- car_pos = self.car.get_pos()
- car_yaw = getattr(self.car, "_my_yaw", 0.0)
-
- # Calculate camera position in front of robot
- camera_offset_x = 0.3 * np.sin(car_yaw)
- camera_offset_y = 0.3 * np.cos(car_yaw)
- camera_x = car_x + camera_offset_x
- camera_y = car_y + camera_offset_y
- camera_z = car_z + 0.2 # Slightly above robot center
-
-**视线方向计算**
- 系统根据机器人朝向自动计算相机的观察方向,确保相机始终朝向机器人的前进方向:
-
-.. code-block:: python
-
- # Calculate look-at point in forward direction
- lookat_x = camera_x + np.sin(car_yaw)
- lookat_y = camera_y + np.cos(car_yaw)
- lookat_z = camera_z
-
-**多视角支持**
- 系统支持多种相机安装模式:
- - 前置相机:安装在机器人前方,用于导航和避障
- - 顶置相机:俯视角度,用于全局定位和地图构建
- - 侧置相机:侧面视角,用于环境感知和物体检测
-
-图像采集系统
-~~~~~~~~~~~
-
-**RGB图像采集**
- 系统提供高质量的RGB图像采集功能,支持多种分辨率和格式:
-
-.. code-block:: python
-
- def capture_rgb_image(self, width=640, height=480, save_to_disk=True):
- # Render RGB image
- self.camera.render()
- rgb_image = self.camera.get_picture("Color")
-
- # Format conversion and processing
- if hasattr(rgb_image, "cpu"):
- rgb_image = rgb_image.cpu().numpy()
-
- # Image post-processing
- rgb_image = (rgb_image * 255).astype(np.uint8)
- rgb_image = cv2.cvtColor(rgb_image, cv2.COLOR_RGB2BGR)
-
- return rgb_image
-
-**深度图像采集**
- 深度图像提供了精确的距离信息,支持机器人的3D感知能力:
-
-.. code-block:: python
-
- def capture_depth_image(self, width=640, height=480, save_to_disk=True):
- # Render depth image
- self.camera.render()
- depth_image = self.camera.get_picture("Depth")
-
- # Depth value processing
- if hasattr(depth_image, "cpu"):
- depth_image = depth_image.cpu().numpy()
-
- # Depth range normalization
- min_depth = np.min(depth_image)
- max_depth = np.max(depth_image)
-
- return depth_image, min_depth, max_depth
-
-**相机参数获取**
- 系统提供完整的相机内参和外参信息:
-
-.. code-block:: python
-
- def get_camera_info(self):
- # Get camera intrinsic matrix
- K = self.camera.get_intrinsic_matrix()
-
- # Get camera extrinsic information
- cam_pos = self.camera.get_pos()
- cam_lookat = self.camera.get_lookat()
-
- return {
- "intrinsic_matrix": K.tolist(),
- "position": cam_pos,
- "lookat": cam_lookat,
- "fov": self.camera.fov,
- "resolution": [self.camera.W, self.camera.H]
- }
\ No newline at end of file
diff --git a/docs/source/index.rst b/docs/source/index.rst
deleted file mode 100644
index 8313eaf..0000000
--- a/docs/source/index.rst
+++ /dev/null
@@ -1,51 +0,0 @@
-robonix OS 开发文档
-=====================
-
-.. admonition:: 项目信息
- :class: info
-
- 代码仓库:https://github.com/HustWolfzzb/robonix
-
- Syswonder 社区:https://www.syswonder.org
-
-robonix OS(DEOS)是由 Syswonder 开发的、面向具身智能(Embodied AI)场景进行通用编程的具身操作系统(Embodied OS),提供了包括编程模型虚拟实体图(Virtual Entity Graph,VEG),Capbility-Skill-Action-Task(CSAT)抽象层次、Action 程序语法与运行时,以及基于 Genesis 模拟器和 Piper Ranger 四轮小车的完整运行框架。
-
-**Overview Structure of robonix OS**
-
-.. image:: _static/uapi_framework.png
- :width: 80%
- :alt: Overview Structure of robonix OS
- :align: center
-
-**文档索引**
-
-.. toctree::
- :maxdepth: 3
- :caption: 模块文档
-
- uapi/index
- manager/index
- genesis/index
-
-.. toctree::
- :maxdepth: 2
- :caption: API 参考文档
-
- api/index
-
-.. toctree::
- :maxdepth: 1
- :caption: 示例代码
-
- examples/simple_demo
-
-.. raw:: html
-
-
-
-索引和表格
-==========
-
-* :ref:`genindex`
-* :ref:`modindex`
-* :ref:`search`
\ No newline at end of file
diff --git a/docs/source/manager/index.rst b/docs/source/manager/index.rst
deleted file mode 100644
index 64719b0..0000000
--- a/docs/source/manager/index.rst
+++ /dev/null
@@ -1,8 +0,0 @@
-Piper ROS2 管理器
-==================
-
-基于 Jeston Orin AGX 的 Piper Ranger 小车管理器
-
-`manager/boot.py`
-
-本部分内容仍在施工中 ...
\ No newline at end of file
diff --git a/docs/source/uapi/action_system.rst b/docs/source/uapi/action_system.rst
deleted file mode 100644
index 53ba860..0000000
--- a/docs/source/uapi/action_system.rst
+++ /dev/null
@@ -1,165 +0,0 @@
-Action 编程模型
-========
-
-Action 是使用 Python 语法的用户编程语言,只需使用 ``@action`` 装饰器标记函数,即可将函数转换为可管理的 Action。
-
- 每个 Action 程序在独立的线程中执行,避免阻塞主程序流程。系统提供完整的并发控制机制,支持 Action 的并行执行和结果同步。
- 系统内置了完整的异常捕获和处理机制,包括彩色日志输出、堆栈跟踪和错误状态管理,确保系统的健壮性。
- 通过 ``EOS_TYPE_ActionResult`` 枚举定义标准的Action执行结果,包括成功(SUCCESS)、失败(FAILURE)和中止(ABORT)三种状态。
-
-Action 装饰器机制
--------------
-
-@action 装饰器
-~~~~~~~~~~~~~
-
-``@action`` 装饰器是 Action 系统的核心组件,负责将普通函数转换为 Action 函数:
-
-.. code-block:: python
-
- @action
- def my_action(param1: str, param2: int) -> EOS_TYPE_ActionResult:
- # Implement business logic
- action_print(f"Executing with {param1} and {param2}")
- return EOS_TYPE_ActionResult.SUCCESS
-
-装饰器会在函数上添加 ``_is_action`` 属性,用于运行时识别Action函数。同时保持原函数的名称和其他属性。
-
-执行模型
------------
-
-运行时系统为每个Action创建独立的守护线程:
-
-.. code-block:: python
-
- def action_worker():
- try:
- result = action_func(*args, **kwargs)
- self.action_results[action_name] = result
- except Exception as e:
- self.action_results[action_name] = None
- logger.error(f"action {action_name} failed: {str(e)}")
-
-系统维护全局的结果字典,支持多种结果查询方式:
-
-- ``wait_for_action()`` - 等待单个Action完成
-- ``wait_for_all_actions()`` - 等待所有Action完成
-- ``get_action_status()`` - 查询Action执行状态
-
-线程被标记为守护线程,确保主程序退出时不会被阻塞。系统提供完整的线程状态监控和清理机制。
-
-Action 日志
------------
-
-action_print 函数
-~~~~~~~~~~~~~~~~
-
-``action_print`` 是专为Action系统设计的日志函数,提供了丰富的上下文信息:
-
-**自动上下文识别**
- 通过堆栈检查自动识别调用的Action函数名称,无需手动指定上下文。
-
-**多级日志支持**
- 支持DEBUG、INFO、WARN、ERROR、CRITICAL五个日志级别,每个级别都有对应的颜色编码。
-
-**双重输出机制**
- - 控制台输出:彩色格式化,便于实时监控
- - 文件输出:完整信息记录,便于后续分析,输出到 action 代码同目录的 action 名称 .log 文件中。
-
-**文件自动管理**
- 根据Action名称自动创建日志文件,文件位置基于Action程序的路径确定。
-
-Runtime 系统
--------------
-
-**运行时实例管理**
- 系统维护全局的运行时实例,通过 ``get_runtime()`` 和 ``set_runtime()`` 函数进行管理:
-
-.. code-block:: python
-
- # Get current runtime instance
- runtime = get_runtime()
-
- # Set new runtime instance
- set_runtime(new_runtime)
-
-**Action程序加载**
- 运行时支持动态加载Action程序文件:
-
- 1. 读取程序文件内容
- 2. 创建独立的模块命名空间
- 3. 执行代码并识别Action函数
- 4. 建立函数名称到实现的映射
-
-
-Type Safety
------------
-
- 虽然 Action 函数本身不强制类型检查,但通过实体的技能调用会进行严格的类型验证。Action函数应返回 ``EOS_TYPE_ActionResult`` 枚举值,系统会在异常情况下自动返回FAILURE状态。
-
-.. important::
- Action 执行时会设置当前实体上下文,使 cap/skill 的具体实现函数能够访问 ``self_entity`` 参数,以实现对自身绑定的其他 skill 和 cap 的调用。
-
-使用示例
--------
-
-以下展示了Action系统的完整使用流程:
-
-**定义Action程序** (example.action)
-
-.. code-block:: python
-
- from uapi.runtime.action import action, EOS_TYPE_ActionResult, get_runtime, action_print
- from uapi.specs.types import EntityPath
-
- @action
- def move_and_capture(robot_path: EntityPath) -> EOS_TYPE_ActionResult:
- runtime = get_runtime()
- robot = runtime.get_graph().get_entity_by_path(robot_path)
-
- if robot is None:
- action_print(f"Robot not found at path: {robot_path}", "ERROR")
- return EOS_TYPE_ActionResult.FAILURE
-
- # Get current position
- current_pos = robot.cap_space_getpos()
- action_print(f"Current position: {current_pos}")
-
- # Move to target position
- move_result = robot.cap_space_move(x=5.0, y=3.0, z=0.0)
- if not move_result["success"]:
- action_print(f"Move failed: {move_result}", "ERROR")
- return EOS_TYPE_ActionResult.FAILURE
-
- # Capture image
- robot.cap_save_rgb_image(filename="captured.jpg", camera_name="camera0")
- action_print("Image captured successfully")
-
- return EOS_TYPE_ActionResult.SUCCESS
-
-**加载和执行Action**
-
-.. code-block:: python
-
- from robonix.uapi import get_runtime, set_runtime
-
- # Get runtime instance
- runtime = get_runtime()
-
- # Build entity graph and set runtime
- runtime.build_entity_graph("my_scene")
- set_runtime(runtime)
-
- # Load action program
- action_names = runtime.load_action_program("example.action")
- print(f"Loaded actions: {action_names}")
-
- # Configure action parameters
- runtime.configure_action("move_and_capture", robot_path="/robot")
-
- # Execute action
- thread = runtime.execute_action("move_and_capture")
-
- # Wait for execution completion
- result = runtime.wait_for_action("move_and_capture")
- print(f"Action result: {result}")
\ No newline at end of file
diff --git a/docs/source/uapi/graph_system.rst b/docs/source/uapi/graph_system.rst
deleted file mode 100644
index 4351529..0000000
--- a/docs/source/uapi/graph_system.rst
+++ /dev/null
@@ -1,125 +0,0 @@
-Virtual Entity Graph(VEG)虚拟实体图
-===================================
-
-虚拟实体图(VEG)系统是UAPI的核心建模组件,通过层次化的实体结构来表示具身应用的认知世界。该系统提供了完整的实体生命周期管理、关系建模和技能绑定机制。
-
-**层次化建模**
- 实体图采用树状层次结构组织世界中的所有对象。根节点通常是一个房间(Room)实体,代表整个环境空间,其他实体作为子节点按照空间或逻辑关系进行组织。这种设计直观地反映了现实世界的组织结构。
-
-**实体类型系统**
- 系统定义了多种实体类型以适应不同的建模需求:
-
- - ``GENERIC`` - 通用实体,适用于一般对象
- - ``CONTROLLABLE`` - 可控实体,通常代表机器人或可操作设备
- - ``COMPUTING`` - 计算实体,代表具有计算能力的组件
- - ``SYSTEM`` - 系统实体,代表系统级组件
- - ``HUMAN`` - 人类实体,代表人类参与者
- - ``ROOM`` - 房间实体,代表空间容器
-
-**关系建模机制**
- 实体间通过关系类型进行连接,目前主要支持父子关系(``PARENT_OF``/``CHILD_OF``),未来可扩展支持更多关系类型如邻接关系、包含关系等。
-
-核心组件
--------
-
-Entity
-~~~~~~~~~
-
-Entity类是实体图的基础构建块,每个实体包含以下核心属性:
-
-**基础属性**
- - ``entity_id`` - 唯一标识符,使用UUID生成
- - ``entity_type`` - 实体类型,决定实体的基本行为特征
- - ``entity_name`` - 实体名称,用于路径寻址和用户识别
- - ``metadata`` - 元数据,存储描述信息和标签
-
-**关系管理**
- 每个实体维护一个关系字典,记录与其他实体的连接关系。关系是双向的,添加子实体时会自动建立相互的父子关系。
-
-**技能绑定**
- 实体通过 ``skills`` 列表和 ``skill_bindings`` 字典管理绑定的技能。技能绑定时会进行规范验证,确保技能的正确性。
-
-EntityPath
-~~~~~~~~~~~
-
-实体图提供了基于路径的寻址机制,类似文件系统的路径结构:
-
-.. code-block:: python
-
- # Path examples
- root = create_root_room() # Path: /
- robot = create_controllable_entity("robot") # Path: /robot
- camera = create_controllable_entity("camera") # Path: /robot/camera
-
- root.add_child(robot)
- robot.add_child(camera)
-
-路径寻址支持相对路径和绝对路径查找,通过 ``get_entity_by_path()`` 方法可以快速定位任意实体。
-
-Skill Binding 机制
------------
-
-**绑定验证**
- 实体绑定技能时,系统会根据技能规范进行严格的验证:
-
- - 检查技能是否在标准规范中定义
- - 验证绑定函数的签名是否符合要求
- - 确保依赖的其他技能已正确绑定
-
-**动态调用**
- 通过Python的 ``__getattr__`` 方法,实体支持动态技能调用。当访问实体的技能属性时,系统会自动创建调用包装器,处理参数验证、类型转换和结果检查。
-
-**参数类型处理**
- 系统支持复杂的参数类型验证和自动转换:
-
- - 基础类型(int, float, str, bool)的自动转换
- - 复合类型(dict, list, tuple)的递归验证
- - 数据类和枚举类型的严格检查
- - 多选类型的灵活匹配
-
-Entity Factory Functions
------------
-
-为了简化实体创建过程,系统提供了一系列工厂函数:
-
-.. code-block:: python
-
- # Create different types of entities
- generic_entity = create_generic_entity("object1")
- controllable_entity = create_controllable_entity("robot1")
- computing_entity = create_computing_entity("computer1")
- human_entity = create_human_entity("user1")
- room_entity = create_room_entity("living_room", room_type="residential")
- root_room = create_root_room()
-
-这些工厂函数自动处理ID生成、类型设置等细节,让用户专注于业务逻辑的实现。
-
-Example
--------
-
-以下是一个典型的实体图构建示例:
-
-.. code-block:: python
-
- from uapi.graph.entity import create_root_room, create_controllable_entity
-
- # Create root room
- root_room = create_root_room()
-
- # Create robot entity
- robot = create_controllable_entity("robot")
- root_room.add_child(robot)
-
- # Create camera entity
- camera = create_controllable_entity("camera")
- robot.add_child(camera)
-
- # Bind skill (Skill Binding)
- def get_pose_impl():
- return {"x": 0.0, "y": 0.0, "z": 0.0}
-
- robot.bind_skill("cap_space_getpos", get_pose_impl)
-
- # Use skill (Skill Usage)
- position = robot.cap_space_getpos()
- print(f"Robot position: {position}")
\ No newline at end of file
diff --git a/docs/source/uapi/index.rst b/docs/source/uapi/index.rst
deleted file mode 100644
index 127089b..0000000
--- a/docs/source/uapi/index.rst
+++ /dev/null
@@ -1,27 +0,0 @@
-UAPI 用户编程接口
-=================
-
-UAPI(User Application Programming Interface)是Robonix OS 的核心用户编程模块,提供了一套完整的具身应用开发接口。该模块实现了分层架构设计,通过实体图建模、运行时管理、技能系统和动作框架等组件。
-
-UAPI模块的设计遵循以下核心原则:
-
-1. 系统以实体(Entity)为核心构建具身应用的认知模型。每个实体代表环境中的一个对象或组件,通过层次化的树状结构组织,形成完整的世界状态表示。实体不仅承载状态信息,还绑定相应的技能和能力。
-
-2. UAPI将具身应用的行为能力抽象为技能(Skill)和能力(Capability)。能力代表基础的原子操作,技能则是由多个能力组合而成的复合行为。
-
-3. 通过 Python 装饰器提供声明式的动作编程接口。开发者只需使用 ``@action`` 装饰器标记函数,即可将其转换为可执行的动作程序,被 UAPI 加载器加载并管理。
-
-.. raw:: html
-
-
-
-UAPI 子模块
-==============
-
-.. toctree::
- :maxdepth: 2
-
- graph_system
- runtime_system
- skill_system
- action_system
\ No newline at end of file
diff --git a/docs/source/uapi/runtime_system.rst b/docs/source/uapi/runtime_system.rst
deleted file mode 100644
index 9386b92..0000000
--- a/docs/source/uapi/runtime_system.rst
+++ /dev/null
@@ -1,93 +0,0 @@
-Runtime
-==========
-
-运行时系统是UAPI的执行引擎,负责管理实体图的生命周期、动作程序的加载执行以及技能提供者的注册管理。该系统采用单例模式设计,提供了完整的运行时环境和管理接口。
-
-数据结构
--------
-
-Runtime 类(单例)
-~~~~~~~~~~~~~~~~~~
-
-Runtime类是运行时系统的核心,采用单例模式确保全局唯一性:
-
-**实体图管理**
- Runtime维护着全局的实体图引用,提供图的设置、获取和钩子管理功能。钩子机制允许在图初始化完成后自动执行自定义逻辑,为系统扩展提供了便利。
-
-**动作程序管理**
- 支持多个动作程序的并存和切换。每个程序都有独立的模块命名空间,包含程序路径、动作函数列表和加载时间等元信息。这种设计支持大型项目的模块化开发。
-
-**并发执行控制**
- 内置多线程支持,每个动作在独立线程中执行。系统维护线程池和结果集合,提供完整的并发控制和状态监控能力。
-
-**实体构建器注册**
- 通过构建器模式支持不同的实体图构建策略。用户可以注册多个构建器函数,根据不同场景选择合适的构建方式。
-
-**统一管理接口**
- 提供了程序加载、动作配置、场景导出等统一的管理接口,简化了复杂系统的操作流程。
-
-Skill Registry System
------------
-
-Registry 和 SkillProvider
-~~~~~~~~~~~~~~~~~~~~~~~~
-
-技能注册系统采用提供者模式设计:
-
-**SkillProvider**
- 技能提供者封装了一组相关的技能实现,包含提供者名称、网络地址和技能列表。这种设计支持分布式的技能部署和管理。
-
-**Registry**
- 注册表管理所有的技能提供者,支持提供者的添加、查找和管理。通过注册表,系统可以动态发现和使用可用的技能。
-
-Action Program Loading
------------
-
-系统使用Python的动态模块机制加载动作程序。每个程序文件被解析为独立的模块对象,拥有自己的命名空间和执行环境。通过反射机制扫描模块中的函数,识别带有 ``_is_action`` 属性的动作函数。这种设计确保了只有正确标记的函数才会被识别为动作。系统支持在运行时切换不同的动作程序,无需重启即可加载新的业务逻辑。这为开发和调试提供了极大的便利。
-
-Hook Extension
------------
-
- 在实体图设置完成后自动执行的扩展点。钩子函数接收Runtime实例作为参数,可以对图进行进一步的配置和初始化。支持钩子的动态添加和移除。如果图已经初始化,新添加的钩子会立即执行,保证了系统状态的一致性。
-
-State Export
------------
-
- 系统可以将当前的实体图结构导出为JSON格式,包含实体层次、绑定技能、关系信息等完整的图状态。支持将技能规范信息导出,便于文档生成和系统集成。
- 提供完整的运行时状态导出,包括图信息、程序状态、动作参数等,支持系统的调试和监控。
-
-Example
--------
-
-以下展示了运行时系统的典型使用方式:
-
-.. code-block:: python
-
- from robonix.uapi import get_runtime, set_runtime
-
- # Get globally unique runtime instance
- runtime = get_runtime()
-
- # Register entity builder
- def my_builder(runtime, **kwargs):
- from robonix.uapi.graph.entity import create_root_room, create_controllable_entity
- root = create_root_room()
- robot = create_controllable_entity("robot")
- root.add_child(robot)
- runtime.set_graph(root)
-
- runtime.register_entity_builder("my_scene", my_builder)
-
- # Build entity graph
- runtime.build_entity_graph("my_scene")
-
- # Set global runtime
- set_runtime(runtime)
-
- # Load action program
- action_names = runtime.load_action_program("my_actions.action")
-
- # Configure and execute action
- runtime.configure_action("my_action", param1="value1")
- runtime.execute_action("my_action")
-
diff --git a/docs/source/uapi/skill_system.rst b/docs/source/uapi/skill_system.rst
deleted file mode 100644
index e26487b..0000000
--- a/docs/source/uapi/skill_system.rst
+++ /dev/null
@@ -1,143 +0,0 @@
-Cap-Skill-Action 系统
-========
-
-技能系统是UAPI的行为建模核心,通过标准化的技能规范和类型安全的绑定机制,为具身应用提供了丰富的行为能力。该系统将复杂的行为抽象为可复用的技能组件,支持组合式的能力构建。
-
-设计原理
--------
-
-**能力与技能分层**
- 系统将具身应用的行为能力分为两个层次:
-
- - **能力(Capability)** - 原子级的基础操作,如获取位置、移动、拍照等。能力通常对应硬件接口或底层服务,是不可再分解的最小行为单元。
- - **技能(Skill)** - 由多个能力组合而成的复合行为,如导航到目标、检测物体等。技能封装了业务逻辑,可以调用其他技能和能力。
-
-**声明式规范定义**
- 所有技能都通过声明式规范进行定义,包含描述、类型、输入输出规格和依赖关系。这种方式确保了技能接口的一致性和可验证性。
-
-**类型安全保障**
- 系统实现了完整的类型检查机制,支持复杂类型的验证和自动转换,包括基础类型、复合类型、数据类和枚举等。
-
-技能规范系统
------------
-
-EOS_SKILL_SPECS
-~~~~~~~~~~~~~~~
-
-关于技能规范的更多信息,请参见 :doc:`../api/skill_specs_table` 章节。
-
-技能规范是一个全局字典,定义了所有标准技能的接口规格:
-
-.. code-block:: python
-
- EOS_SKILL_SPECS = {
- "cap_space_getpos": {
- "description": "Get the position of the entity",
- "type": EOS_SkillType.CAPABILITY,
- "input": None,
- "output": {"x": float, "y": float, "z": float},
- },
- "skl_detect_objs": {
- "description": "Detect objects in the current view",
- "type": EOS_SkillType.SKILL,
- "input": {"camera_name": str},
- "output": Dict[str, Tuple[float, float, float]],
- "dependencies": ["cap_camera_dep_rgb", "cap_camera_info"],
- }
- }
-
-**规范字段说明**
- - ``description`` - 技能的功能描述
- - ``type`` - 技能类型(CAPABILITY 或 SKILL)
- - ``input`` - 输入参数规格,支持None、字典或列表(多选类型)
- - ``output`` - 输出结果规格
- - ``dependencies`` - 依赖的其他技能列表(仅技能类型需要)
-
-类型系统
--------
-
-**基础类型支持**
- 系统支持Python的所有基础类型,包括int、float、str、bool等,并提供自动类型转换功能。
-
-**复合类型处理**
- 支持复杂的数据结构:
-
- - **字典类型** - 定义结构化数据的字段和类型
- - **列表类型** - 支持同质元素的集合
- - **元组类型** - 支持异构元素的有序组合
- - **数据类** - 支持用户定义的数据结构
- - **枚举类型** - 支持有限选项的类型安全
-
-**多选类型机制**
- 通过列表定义多种可接受的输入格式:
-
-.. code-block:: python
-
- "input": [
- None, # 无参数调用
- {"timeout_sec": float} # 带超时参数调用
- ]
-
-技能绑定机制
------------
-
-**绑定验证流程**
- 实体绑定技能时经过严格的验证:
-
- 1. 检查技能是否在标准规范中定义
- 2. 验证绑定函数的存在性和可调用性
- 3. 将技能添加到实体的技能列表中
- 4. 建立技能名称到函数的映射关系
-
-**动态调用包装**
- 通过 ``__getattr__`` 方法实现动态调用:
-
-.. code-block:: python
-
- def __getattr__(self, name):
- if name in self.skill_bindings:
- def wrapper(**kwargs):
- # 参数验证
- self._check_skill_args(name, kwargs)
- # 函数调用
- result = self.skill_bindings[name](**kwargs)
- # 结果验证
- self._check_skill_returns(name, result)
- return result
- return wrapper
-
-**自实体注入机制**
- 对于需要访问实体上下文的技能,系统支持自动注入 ``self_entity`` 参数,使技能函数能够访问调用实体的状态和其他技能。
-
-使用示例
--------
-
-以下展示了技能系统的典型使用方式:
-
-.. code-block:: python
-
- from uapi.graph.entity import create_controllable_entity
-
- # Create entity
- robot = create_controllable_entity("robot")
-
- # Define skill implementations
- def get_position():
- # Actual position retrieval logic
- return {"x": 1.0, "y": 2.0, "z": 0.0}
-
- def move_to_position(x, y, z):
- # Actual movement logic
- print(f"Moving to ({x}, {y}, {z})")
- return {"success": True}
-
- # Bind skills
- robot.bind_skill("cap_space_getpos", get_position)
- robot.bind_skill("cap_space_move", move_to_position)
-
- # Use skills
- current_pos = robot.cap_space_getpos()
- print(f"Current position: {current_pos}")
-
- result = robot.cap_space_move(x=5.0, y=3.0, z=0.0)
- print(f"Move result: {result}")
\ No newline at end of file
diff --git a/pyrightconfig.json b/pyrightconfig.json
new file mode 100644
index 0000000..543d4ce
--- /dev/null
+++ b/pyrightconfig.json
@@ -0,0 +1,15 @@
+{
+ "include": [
+ "rust/**/*.py"
+ ],
+ "exclude": [
+ "**/node_modules",
+ "**/__pycache__",
+ "**/.*"
+ ],
+ "extraPaths": [
+ "rust/robonix-sdk/build/robonix_sdk/ament_cmake_python/robonix_sdk"
+ ],
+ "pythonVersion": "3.10",
+ "typeCheckingMode": "basic"
+}
diff --git a/rust/Makefile b/rust/Makefile
index 3682310..219ee6b 100644
--- a/rust/Makefile
+++ b/rust/Makefile
@@ -2,8 +2,11 @@
# Robonix Makefile
#
# Convenient commands for building and running Robonix components
+#
+# Tab completion: bash-completion is installed in Docker container
+# If tab completion doesn't work, ensure bash-completion is installed and enabled
-.PHONY: help build build-cli build-core build-sdk debug release install install-cli install-core source-sdk env clean test check setup-dev fmt
+.PHONY: help build build-cli build-core build-sdk debug release install install-cli install-core source-sdk env clean test test-all test-rust test-python test-cpp test-ros2 benchmark check setup-dev fmt test-framework
.DEFAULT_GOAL := help
# Default ROS2 distribution (can be overridden)
@@ -49,12 +52,22 @@ help:
@echo " make check - Run cargo check on all Rust projects"
@echo " make clean - Clean build artifacts"
@echo ""
+ @echo "Test commands:"
+ @echo " make test - Run all tests (Rust, Python)"
+ @echo " make test-rust - Run Rust CLI stress tests"
+ @echo " make test-python - Run Python stress tests"
+ @echo " make benchmark - Run benchmark suite with different concurrency levels"
+ @echo ""
@echo "Examples:"
@echo " make build"
@echo " make install"
@echo " rbnx config --show"
@echo " rbnx package list"
@echo " eval \$$(make source-sdk)"
+ @echo ""
+ @echo "Tab Completion:"
+ @echo " Tab completion for make targets is enabled automatically in Docker container"
+ @echo " If it doesn't work, ensure bash-completion package is installed"
# Build mode helper: determine cargo build flags
ifeq ($(BUILD_MODE),release)
@@ -186,9 +199,9 @@ env:
setup-dev:
@echo "Setting up development environment..."
@mkdir -p ~/.robonix
- @if [ -L ~/.robonix/packages ]; then \
- echo "Removing existing provider link..."; \
- rm -f ~/.robonix/packages; \
+ @if [ -e ~/.robonix/packages ]; then \
+ echo "Removing existing packages path..."; \
+ rm -rf ~/.robonix/packages; \
fi
@ln -sf "$(PROVIDER_DIR)" ~/.robonix/packages
@echo "✓ Linked $(PROVIDER_DIR) to ~/.robonix/packages"
@@ -212,11 +225,57 @@ check:
cd $(CORE_DIR) && cargo check
@echo "✓ All checks passed"
-test:
- @echo "Running tests..."
- cd $(CLI_DIR) && cargo test || true
- cd $(CORE_DIR) && cargo test || true
- @echo "✓ Tests completed"
+# Test framework paths
+TEST_FRAMEWORK_DIR := $(RUST_DIR)/tools/test_framework
+TEST_RUST_DIR := $(TEST_FRAMEWORK_DIR)/rust_tests
+TEST_PYTHON_DIR := $(TEST_FRAMEWORK_DIR)/python_tests
+TEST_CPP_DIR := $(TEST_FRAMEWORK_DIR)/cpp_tests
+TEST_ROS2_DIR := $(TEST_FRAMEWORK_DIR)/ros2_tests
+
+# Test parameters (can be overridden)
+CONCURRENCY ?= 3
+REQUESTS ?= 1000
+RATE ?= 100
+DURATION ?= 0
+
+# Test commands
+test: test-all
+
+test-all: test-rust test-python
+ @echo "✓ All tests completed"
+
+test-rust:
+ @echo "Running Rust CLI stress tests (concurrency=$(CONCURRENCY), requests=$(REQUESTS))..."
+ @if [ -f "$(TEST_FRAMEWORK_DIR)/test.sh" ]; then \
+ bash $(TEST_FRAMEWORK_DIR)/test.sh rust rustdds $(CONCURRENCY) $(REQUESTS) $(RATE) $(DURATION); \
+ else \
+ echo "Test framework not found. Run 'make test-framework' first."; \
+ exit 1; \
+ fi
+
+test-python:
+ @echo "Running Python stress tests (concurrency=$(CONCURRENCY), requests=$(REQUESTS))..."
+ @if [ -f "$(TEST_FRAMEWORK_DIR)/test.sh" ]; then \
+ bash $(TEST_FRAMEWORK_DIR)/test.sh python rustdds $(CONCURRENCY) $(REQUESTS) $(RATE) $(DURATION); \
+ else \
+ echo "Test framework not found. Run 'make test-framework' first."; \
+ exit 1; \
+ fi
+
+
+benchmark:
+ @echo "Running benchmark suite with different concurrency levels..."
+ @if [ -f "$(TEST_FRAMEWORK_DIR)/benchmark.sh" ]; then \
+ bash $(TEST_FRAMEWORK_DIR)/benchmark.sh; \
+ else \
+ echo "Benchmark script not found."; \
+ exit 1; \
+ fi
+
+test-framework:
+ @echo "Setting up test framework..."
+ @echo "✓ Test framework ready"
+ @echo "Run 'make test' to execute all tests"
# Clean commands
clean:
diff --git a/rust/PACKAGE_SPEC.md b/rust/PACKAGE_SPEC.md
index a3fbedd..0cd70f4 100644
--- a/rust/PACKAGE_SPEC.md
+++ b/rust/PACKAGE_SPEC.md
@@ -152,7 +152,7 @@ primitives:
start_script: rbnx/start_arm_move_v2.sh
stop_script: rbnx/stop_arm_move_v2.sh
- - name: prm::camera.capture
+ - name: prm::camera.rgb
# Spec definition: OUTPUT: {"image":"sensor_msgs/Image"}
input_schema: '{}'
output_schema: '{"image":"/camera/image"}'
diff --git a/rust/README.md b/rust/README.md
index 69e9118..547138d 100644
--- a/rust/README.md
+++ b/rust/README.md
@@ -60,41 +60,33 @@ make setup-dev
## Step 3: Start robonix-core
-robonix-core provides the core services for the system. You need to start it in a separate terminal before using the CLI.
+robonix-core provides the core services for the system. You need to start it in a separate terminal before using the CLI. **It does not take command-line flags**; behavior is controlled by **environment variables**.
+
+From the `rust` directory, run:
```bash
-# In terminal 1: Start robonix-core
-eval $(make source-sdk) # source robonix-sdk environment
+# In terminal 1: source SDK, then start robonix-core with web UI
+cd rust
+eval $(make source-sdk)
+ROBONIX_WEB_ASSETS_DIR="$(pwd)/robonix-core/web" \
+ROBONIX_WEB_PORT=8000 \
+RUST_LOG=robonix_core=info \
robonix-core
```
-robonix-core will start the following services:
+- **ROBONIX_WEB_ASSETS_DIR** and **ROBONIX_WEB_PORT**: Required for the web management UI. If either is unset, robonix-core runs without the web server (ROS2 services only).
+- **RUST_LOG**: Optional; controls log level (e.g. `robonix_core=info`, `robonix_core=debug`, `robonix_core::task=debug`, or `debug` for all).
+
+Alternatively use the helper script from `rust`: `./core.sh` (starts in background with the same env).
+
+robonix-core will start:
- **Primitive API** (`/rbnx/prm/*`): Primitive registration and query
- **Service API** (`/rbnx/srv/*`): Standard service registration and query
- **Skill API** (`/rbnx/skl/*`): Skill registration and query
- **Task API** (`/rbnx/task/*`): Task submission, status query, and result retrieval
+- **Web UI** (when env vars above are set): http://localhost:8000
Keep this terminal running.
-
-### Configuring Log Levels
-
-You can control the verbosity of robonix-core logs by setting the `RUST_LOG` environment variable:
-
-```bash
-# Show only info, warn, and error messages (default)
-robonix-core
-
-# Show debug messages for robonix-core module
-RUST_LOG=robonix_core=debug robonix-core
-# Show debug messages for all modules, you can see rustdds logs too for example
-RUST_LOG=debug robonix-core
-# Show debug for task_manager only
-RUST_LOG=robonix_core::task_manager=debug robonix-core
-# Show trace messages (most verbose)
-RUST_LOG=robonix_core=trace robonix-core
-# Customize log levels for different modules
-RUST_LOG=robonix_core::task_manager=debug,robonix_core=info,rustdds=error robonix-core
-```
## Step 4: Configure robonix-cli
In a new terminal (terminal 2), configure the CLI:
@@ -123,6 +115,7 @@ rbnx package list
# View package details
rbnx package info
# Build all packages
+rbnx package build
rbnx package build all
```
@@ -178,17 +171,14 @@ When you create a task, the system automatically:
4. **Executes** the RTDL code, calling skills in sequence (status: `running`)
5. **Completes** with result feedback (status: `finished` or `failed`)
-## Step 9: Using Standard Services
+## Step 9: Standard Services (Used in Task Flow)
-The system provides standard services that can be queried:
+The task manager **currently uses** these standard services during task execution:
-- **Spatial Map Service** (`spatial_map`): Geometric structure information
-- **Semantic Map Service** (`semantic_map`): Object-level environment representation
-- **Task Planning Service** (`task_plan`): Converts natural language to RTDL code
-- **Plan Simulation Service** (`plan_simulate`): Validates task plan feasibility
-- **Result Feedback Service** (`result_feedback`): Validates execution results
+- **Semantic Map Service** (`srv::semantic_map`): Object-level environment representation; core polls it to build the object graph used by planning.
+- **Task Planning Service** (`srv::task_plan`): Converts natural language to RTDL (list format); core calls it in the planning phase.
-These services are automatically called by the task manager during task execution. You can also query them directly via ROS2 services.
+Other standard service **specs** (e.g. `spatial_map`, `plan_simulate`, `result_feedback`) are defined for providers but are **not yet invoked** in the core task flow. You can query registered services via the Web UI or ROS2.
## Common Commands Reference
@@ -211,7 +201,8 @@ make install-core # Install robonix-core binary only
```bash
rbnx # Run CLI with any command
rbnx-daemon # Run daemon
-robonix-core # Run robonix-core
+# robonix-core: set ROBONIX_WEB_ASSETS_DIR and ROBONIX_WEB_PORT for web UI (see Step 3)
+./core.sh # From rust/: start robonix-core in background with web UI
```
### Environment Commands
@@ -245,7 +236,8 @@ ros2 service type /rbnx/task/submit
### Clean up all ROS2 processes
```bash
-pkill -9 -f "ros2|robonix|rclpy|rclcpp|demo_rgb_provider"
+pkill -9 -f "ros2|rclpy|rclcpp|webots|python|python3|rbnx-daemon|robonix-core|rviz2"
+rm -f /dev/shm/sem.fastrtps_* /dev/shm/fastrtps_*
```
## System Architecture
@@ -257,23 +249,19 @@ pkill -9 -f "ros2|robonix|rclpy|rclcpp|demo_rgb_provider"
- **Skills**: User-defined high-level action logic, written in RTDL, flexible and do not need to conform to specifications
- Skills can call primitives and services, and can also call other skills
-### RTDL Format
-
-RTDL (Robot Task Description Language) is the task description language. Example:
-
-```python
-def skl::close_window(room: str):
- skl::navigate_to(target_label = room)
- srv::semantic_map.update(entity = room)
- pose = srv::semantic_map.query_pose(
- entity_type = "window",
- parent_room = room
- )
- prm::arm.move.ee(pose = pose)
- prm::gripper.close()
- return True
+### RTDL Format (Task Execution)
+
+The **task executor** currently supports only **list-form RTDL**: a JSON array of instructions. The `task_plan` service returns this format. Each instruction has `object_id`, `type`, `name`, and `params`:
+
+```json
+[
+ { "object_id": "robot_001", "type": "skill", "name": "pick", "params": { "target": "cup_001" } },
+ { "object_id": "robot_001", "type": "skill", "name": "place", "params": { "destination": "table_001" } }
+]
```
+Skills can be implemented as RTDL files (e.g. Python-like syntax in package `main_rtdl`); the executor runs the **list** produced by `task_plan`, which may reference those skills by name.
+
### Data Types
- System supports Robonix custom message types (Point3D, Object, BoundingBox, etc.)
diff --git a/rust/cli.sh b/rust/cli.sh
new file mode 100755
index 0000000..e905b76
--- /dev/null
+++ b/rust/cli.sh
@@ -0,0 +1,9 @@
+rbnx daemon stop 2>/dev/null || true
+
+# make build-sdk
+eval $(make source-sdk)
+rbnx deploy build
+rbnx deploy register demo_recipe.yaml
+rm -rf ./provider/logs
+mkdir -p ./provider/logs
+rbnx deploy restart
\ No newline at end of file
diff --git a/rust/core.sh b/rust/core.sh
new file mode 100755
index 0000000..7670c65
--- /dev/null
+++ b/rust/core.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+
+set -e
+
+export RMW_IMPLEMENTATION=rmw_fastrtps_cpp
+
+cleanup() {
+ echo ""
+ echo "Cleaning up: killing robonix-core, found pid(s): $(pgrep -x robonix-core | sort -n)"
+ pgrep -x robonix-core | sort -n | xargs -r kill -9
+ wait $ROBONIX_PID 2>/dev/null || true
+ echo "Making sure robonix-core does not exist..."
+ if pgrep -x robonix-core >/dev/null; then
+ echo "robonix-core still exists"
+ exit 1
+ fi
+ echo "Cleanup complete!"
+ exit 0
+}
+
+trap cleanup SIGINT SIGTERM
+
+ROBONIX_WEB_ASSETS_DIR="$(pwd)/robonix-core/web" \
+ROBONIX_WEB_PORT=8000 \
+RUST_LOG=robonix_core=info robonix-core &
+ROBONIX_PID=$!
+
+wait $ROBONIX_PID
+
+trap cleanup SIGINT SIGTERM
\ No newline at end of file
diff --git a/rust/demo_recipe.yaml b/rust/demo_recipe.yaml
index 368553f..a368880 100644
--- a/rust/demo_recipe.yaml
+++ b/rust/demo_recipe.yaml
@@ -6,13 +6,28 @@
# arbitrary selection of which primitives, services, and skills to register.
name: demo_recipe
-description: Demo recipe showing how to selectively register primitives, services, and skills from packages
+description: Demo recipe for Tiago robot with semantic map service and navigation skills
+# Note: This recipe requires AMCL to be running for pose primitives to work.
+# AMCL publishes to /amcl_pose topic (PoseWithCovarianceStamped).
packages:
- # Example 1: Register all primitives, services, and skills from a package
- # (when primitives, services, and skills are not specified, all will be registered)
- - name: demo_rgb_provider
- # primitives: [] # If not specified, all primitives will be registered
- # services: [] # If not specified, all services will be registered
- # skills: [] # If not specified, all skills will be registered
- - name: demo_service_provider
\ No newline at end of file
+ # Tiago demo package - provides camera, depth, pose, and navigation primitives
+ # prm::base.pose.cov uses AMCL directly (PoseWithCovarianceStamped from /amcl_pose)
+ - name: tiago_demo_package
+ primitives:
+ - prm::camera.rgb
+ - prm::camera.depth
+ - prm::base.pose.cov # Direct AMCL pose (PoseWithCovarianceStamped from /amcl_pose)
+ - prm::base.navigate
+
+ # Demo service provider - provides semantic_map and task_plan services
+ - name: demo_service_provider
+ services:
+ - srv::semantic_map
+ - srv::task_plan
+
+ # Navigation skills provider - provides wandering and move_to_object skills
+ - name: navigation_skills_provider
+ skills:
+ - skl::wandering
+ - skl::move_to_object
diff --git a/rust/dev_setup.sh b/rust/dev_setup.sh
new file mode 100755
index 0000000..1cc5122
--- /dev/null
+++ b/rust/dev_setup.sh
@@ -0,0 +1,14 @@
+#!/bin/env bash
+# only use this script when you want to test packages under provider directory
+# and only use this script in a fresh docker container booted from docker/run.sh
+
+set -e
+
+make build-sdk
+make install
+make setup-dev
+eval $(make source-sdk)
+rbnx config --set-sdk-path $(realpath ./robonix-sdk)
+rbnx config --show
+rbnx package list
+rbnx package build
diff --git a/rust/kill.sh b/rust/kill.sh
new file mode 100755
index 0000000..8debf41
--- /dev/null
+++ b/rust/kill.sh
@@ -0,0 +1,44 @@
+#!/usr/bin/env bash
+# Force kill: nav, nav2, riv*, realsense, and all .robonix package processes
+
+set -e
+
+# 1. nav / nav2 / navigation2
+pkill -9 -f 'nav2' 2>/dev/null || true
+pkill -9 -f 'navigation2' 2>/dev/null || true
+pkill -9 -f 'lifecycle_manager_navigation' 2>/dev/null || true
+pkill -9 -f 'bt_navigator' 2>/dev/null || true
+pkill -9 -f 'controller_server' 2>/dev/null || true
+pkill -9 -f 'planner_server' 2>/dev/null || true
+pkill -9 -f 'behavior_server' 2>/dev/null || true
+pkill -9 -f 'waypoint_follower' 2>/dev/null || true
+pkill -9 -f 'velocity_smoother' 2>/dev/null || true
+pkill -9 -f 'smoother_server' 2>/dev/null || true
+pkill -9 -f 'global_costmap|local_costmap' 2>/dev/null || true
+
+# 2. rviz
+pkill -9 -f 'rviz2' 2>/dev/null || true
+pkill -9 -f 'rviz' 2>/dev/null || true
+
+# 3. realsense
+pkill -9 -f 'realsense' 2>/dev/null || true
+pkill -9 -f 'realsense2_camera' 2>/dev/null || true
+pkill -9 -f 'camera_435' 2>/dev/null || true
+
+# 4. .robonix packages (from ps -ef: mid360_drv, ranger_drv, navigation2_base, pcld2lscan-rbnx)
+pkill -9 -f '.robonix/packages/mid360_drv' 2>/dev/null || true
+pkill -9 -f '.robonix/packages/ranger_drv' 2>/dev/null || true
+pkill -9 -f '.robonix/packages/navigation2_base' 2>/dev/null || true
+pkill -9 -f '.robonix/packages/pcld2lscan-rbnx' 2>/dev/null || true
+pkill -9 -f 'livox_ros_driver2_node' 2>/dev/null || true
+pkill -9 -f 'ranger_base_node' 2>/dev/null || true
+pkill -9 -f 'pointcloud_to_laserscan_node' 2>/dev/null || true
+pkill -9 -f 'robot_state_publisher' 2>/dev/null || true
+
+# 5. fastrtps shm
+rm -f /dev/shm/sem.fastrtps_* /dev/shm/fastrtps_* 2>/dev/null || true
+
+pkill -9 -f "ros2|rclpy|rclcpp|webots|python|python3|rbnx-daemon|robonix-core|rviz2"
+rm -f /dev/shm/sem.fastrtps_* /dev/shm/fastrtps_*
+
+echo "kill.sh done."
diff --git a/rust/provider/demo_package/README.md b/rust/provider/demo_package/README.md
deleted file mode 100644
index 2591a63..0000000
--- a/rust/provider/demo_package/README.md
+++ /dev/null
@@ -1,149 +0,0 @@
-# Demo RGB Provider Package
-
-SPDX-License-Identifier: MulanPSL-2.0
-
-This package provides demo implementations of Robonix capabilities and skills:
-- **cap::vision.capture_rgb**: RGB camera capability that publishes random color images
-- **cap::grasp.move**: Grasp movement capability
-- **skl::pick**: Pick skill that combines vision and grasp capabilities
-
-## Components
-
-### Capabilities
-
-#### cap::vision.capture_rgb
-
-RGB camera capability that publishes random color images to `/demo_rgb/image` topic.
-
-**Publisher**: `/demo_rgb/image` (sensor_msgs/Image)
-
-#### cap::grasp.move
-
-Grasp movement capability that subscribes to pose goals and publishes status.
-
-**Subscriber**: `/demo_grasp/pose_goal` (geometry_msgs/PoseStamped)
-**Publisher**: `/demo_grasp/pose_status` (std_msgs/Bool)
-
-### Skills
-
-#### skl::pick
-
-Pick skill that combines vision and grasp capabilities to perform pick operations.
-
-**Start Topic**: `/robot1/skill/pick/start` (std_msgs/String)
-**Status Topic**: `/robot1/skill/pick/status` (std_msgs/String)
-
-## Building and Installation
-
-### Prerequisites
-
-- ROS2 (Humble or later)
-- Python 3.8+
-- colcon build tools
-- numpy (for image generation)
-
-### Build
-
-```bash
-# From package directory
-colcon build --packages-select demo_rgb_provider
-```
-
-Or use robonix-cli:
-
-```bash
-rbnx deploy build
-```
-
-### Install Dependencies
-
-The package requires the following Python packages (automatically installed via setup.py):
-- `numpy`: For image array generation
-
-## Usage
-
-### Start Capabilities and Skills
-
-Components can be started individually or via robonix-cli:
-
-```bash
-# Start RGB capture capability
-rbnx deploy start cap::vision.capture_rgb
-
-# Start grasp move capability
-rbnx deploy start cap::grasp.move
-
-# Start pick skill
-rbnx deploy start skl::pick
-
-# Or start all components
-rbnx deploy start all
-```
-
-### Stop Components
-
-```bash
-# Stop specific component
-rbnx deploy stop skl::pick
-
-# Stop all components
-rbnx deploy stop all
-```
-
-## Topics and Services
-
-### Published Topics
-
-- `/demo_rgb/image` (sensor_msgs/Image): RGB camera images
-- `/demo_grasp/pose_status` (std_msgs/Bool): Grasp movement status
-- `/robot1/skill/pick/status` (std_msgs/String): Pick skill execution status
-
-### Subscribed Topics
-
-- `/demo_grasp/pose_goal` (geometry_msgs/PoseStamped): Target pose for grasp movement
-- `/robot1/skill/pick/start` (std_msgs/String): Pick skill start command
-
-## Example Usage
-
-### Using Pick Skill
-
-1. Start the required capabilities:
- ```bash
- rbnx deploy start cap::vision.capture_rgb
- rbnx deploy start cap::grasp.move
- ```
-
-2. Start the pick skill:
- ```bash
- rbnx deploy start skl::pick
- ```
-
-3. Send a pick command:
- ```bash
- ros2 topic pub /robot1/skill/pick/start std_msgs/String "data: 'pick cup_001'"
- ```
-
-4. Monitor skill status:
- ```bash
- ros2 topic echo /robot1/skill/pick/status
- ```
-
-## Troubleshooting
-
-### Components Not Starting
-
-1. Ensure ROS2 is properly sourced: `source /opt/ros/humble/setup.bash`
-2. Check that robonix-sdk is built and sourced
-3. Verify Python dependencies are installed (numpy)
-4. Check component logs in `rbnx/` directory
-
-### Image Not Publishing
-
-1. Verify RGB publisher is running: `ros2 topic list | grep demo_rgb`
-2. Check topic data: `ros2 topic echo /demo_rgb/image`
-3. Ensure numpy is installed: `pip3 install numpy`
-
-## License
-
-MulanPSL-2.0
-
diff --git a/rust/provider/demo_package/demo_rgb_provider/__init__.py b/rust/provider/demo_package/demo_rgb_provider/__init__.py
deleted file mode 100644
index 8409a79..0000000
--- a/rust/provider/demo_package/demo_rgb_provider/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-# SPDX-License-Identifier: MulanPSL-2.0
-# Demo RGB Provider Package
-#
-# Python package initializer for demo_rgb_provider
diff --git a/rust/provider/demo_package/demo_rgb_provider/grasp_move.py b/rust/provider/demo_package/demo_rgb_provider/grasp_move.py
deleted file mode 100644
index 2a25cd6..0000000
--- a/rust/provider/demo_package/demo_rgb_provider/grasp_move.py
+++ /dev/null
@@ -1,83 +0,0 @@
-#!/usr/bin/env python3
-# SPDX-License-Identifier: MulanPSL-2.0
-# Grasp Move Capability
-#
-# Demo grasp move capability package.
-# Subscribes to pose goals and publishes status.
-""""""
-
-import rclpy
-from rclpy.node import Node
-from geometry_msgs.msg import PoseStamped
-from std_msgs.msg import Bool
-import time
-
-
-class GraspMoveCapability(Node):
- """Implements cap::grasp.move capability."""
-
- def __init__(self):
- super().__init__('demo_grasp_move')
-
- # Subscribe to pose goal (input)
- self.pose_goal_subscriber = self.create_subscription(
- PoseStamped,
- '/demo_grasp/pose_goal',
- self.pose_goal_callback,
- 10
- )
-
- # Publish status (output)
- self.status_publisher = self.create_publisher(
- Bool,
- '/demo_grasp/pose_status',
- 10
- )
-
- self.current_status = False
- self.get_logger().info('Demo grasp move capability started')
- self.get_logger().info(' Subscribing to: /demo_grasp/pose_goal')
- self.get_logger().info(' Publishing to: /demo_grasp/pose_status')
-
- def pose_goal_callback(self, msg):
- """Handle incoming pose goal."""
- self.get_logger().info(
- f'Received pose goal: position=({msg.pose.position.x:.2f}, '
- f'{msg.pose.position.y:.2f}, {msg.pose.position.z:.2f})'
- )
-
- # Simulate movement
- self.current_status = False
- self.publish_status(False)
-
- # Simulate movement delay
- time.sleep(0.5)
-
- # Movement complete
- self.current_status = True
- self.publish_status(True)
- self.get_logger().info('Grasp movement completed')
-
- def publish_status(self, status):
- """Publish status."""
- msg = Bool()
- msg.data = status
- self.status_publisher.publish(msg)
-
-
-def main(args=None):
- rclpy.init(args=args)
- grasp_move = GraspMoveCapability()
-
- try:
- rclpy.spin(grasp_move)
- except KeyboardInterrupt:
- pass
- finally:
- grasp_move.destroy_node()
- rclpy.shutdown()
-
-
-if __name__ == '__main__':
- main()
-
diff --git a/rust/provider/demo_package/demo_rgb_provider/pick_skill.py b/rust/provider/demo_package/demo_rgb_provider/pick_skill.py
deleted file mode 100644
index 48c0a46..0000000
--- a/rust/provider/demo_package/demo_rgb_provider/pick_skill.py
+++ /dev/null
@@ -1,327 +0,0 @@
-#!/usr/bin/env python3
-# SPDX-License-Identifier: MulanPSL-2.0
-# Pick Skill
-#
-# Demo pick skill that combines vision and grasp capabilities.
-# Implements EAIOS skill interface with start_topic and status_topic.
-""""""
-
-import rclpy
-from rclpy.node import Node
-from std_msgs.msg import String
-from sensor_msgs.msg import Image
-from geometry_msgs.msg import PoseStamped
-import json
-import time
-import signal
-from robonixpy import RobonixClient
-
-
-class PickSkill(Node):
- """Implements pick skill using EAIOS skill interface."""
-
- def __init__(self):
- super().__init__('demo_pick_skill')
-
- # EAIOS skill interface topics (from manifest)
- self.start_topic = '/robot1/skill/pick/start'
- self.status_topic = '/robot1/skill/pick/status'
-
- # Query primitives and get topic names
- self.vision_image_topic = None
- self.grasp_pose_goal_topic = None
- self.grasp_status_topic = None
-
- # Create Robonix client for service calls
- try:
- self.get_logger().info('Creating RobonixClient...')
- self.robonix_client = RobonixClient(node_name='demo_pick_skill_client', max_workers=20)
- self.get_logger().info('RobonixClient created successfully, querying primitives...')
- # Query primitives
- self._query_primitives()
- except Exception as e:
- import traceback
- error_traceback = traceback.format_exc()
- self.get_logger().error(f'Failed to create RobonixClient: {e}')
- self.get_logger().error(f'Error type: {type(e).__name__}')
- self.get_logger().error(f'Full traceback:\n{error_traceback}')
- import sys
- print(f'[ERROR] Failed to create RobonixClient: {e}', file=sys.stderr)
- self.robonix_client = None
- self._use_fallback_topics()
-
- # Subscribe to skill start topic (receives JSON with parameters)
- self.start_subscriber = self.create_subscription(
- String,
- self.start_topic,
- self.start_callback,
- 10
- )
- self.get_logger().info(f'Subscribing to start topic: {self.start_topic}')
-
- # Publish to skill status topic (publishes JSON with state and result)
- self.status_publisher = self.create_publisher(
- String,
- self.status_topic,
- 10
- )
- self.get_logger().info(f'Publishing to status topic: {self.status_topic}')
-
- # Subscribe to vision primitive output (dynamic topic)
- if self.vision_image_topic:
- self.image_subscriber = self.create_subscription(
- Image,
- self.vision_image_topic,
- self.image_callback,
- 10
- )
- self.get_logger().info(f' Subscribing to vision output: {self.vision_image_topic}')
- else:
- self.image_subscriber = None
- self.get_logger().warn(' Vision primitive not available!')
-
- # Publish to grasp primitive input (dynamic topic)
- if self.grasp_pose_goal_topic:
- self.pose_goal_publisher = self.create_publisher(
- PoseStamped,
- self.grasp_pose_goal_topic,
- 10
- )
- self.get_logger().info(f' Publishing to grasp input: {self.grasp_pose_goal_topic}')
- else:
- self.pose_goal_publisher = None
- self.get_logger().warn(' Grasp primitive not available!')
-
- # Subscribe to grasp primitive output (dynamic topic)
- if self.grasp_status_topic:
- self.grasp_status_subscriber = self.create_subscription(
- String,
- self.grasp_status_topic,
- self.grasp_status_callback,
- 10
- )
- self.get_logger().info(f' Subscribing to grasp output: {self.grasp_status_topic}')
- else:
- self.grasp_status_subscriber = None
-
- self.current_skill_id = None
- self.current_target_label = None
- self.latest_image = None
- self.grasp_complete = False
- self.picking_in_progress = False
-
- self.get_logger().info('Demo pick skill initialized')
-
- def _query_primitives(self):
- """Query robonix core for required primitives and get their topic names."""
- if not self.robonix_client:
- self._use_fallback_topics()
- return
-
- # Query prm::camera.capture
- self.get_logger().info('Querying prm::camera.capture...')
- try:
- response = self.robonix_client.query_primitive('prm::camera.capture')
- if response and response.instances:
- instance = response.instances[0]
- # Parse output_schema to get image topic
- import json
- output_schema = json.loads(instance.output_schema)
- if 'image' in output_schema:
- self.vision_image_topic = output_schema['image']
- self.get_logger().info(f' Found vision primitive: {self.vision_image_topic}')
- except Exception as e:
- import traceback
- self.get_logger().error(f'Error querying prm::camera.capture: {e}')
- self.get_logger().error(f'Traceback:\n{traceback.format_exc()}')
-
- # Query prm::arm.move.ee
- self.get_logger().info('Querying prm::arm.move.ee...')
- try:
- response = self.robonix_client.query_primitive('prm::arm.move.ee')
- if response and response.instances:
- instance = response.instances[0]
- # Parse input_schema and output_schema to get topics
- import json
- input_schema = json.loads(instance.input_schema)
- output_schema = json.loads(instance.output_schema)
- if 'pose' in input_schema:
- self.grasp_pose_goal_topic = input_schema['pose']
- self.get_logger().info(f' Found grasp input topic: {self.grasp_pose_goal_topic}')
- if 'status' in output_schema:
- self.grasp_status_topic = output_schema['status']
- self.get_logger().info(f' Found grasp output topic: {self.grasp_status_topic}')
- except Exception as e:
- import traceback
- self.get_logger().error(f'Error querying prm::arm.move.ee: {e}')
- self.get_logger().error(f'Traceback:\n{traceback.format_exc()}')
-
- # Use fallback topics if query failed
- if not self.vision_image_topic or not self.grasp_pose_goal_topic:
- self.get_logger().warn('Some primitives not found, using fallback topics')
- self._use_fallback_topics()
-
- def _use_fallback_topics(self):
- """Use hardcoded topics as fallback when query service is not available."""
- if not self.vision_image_topic:
- self.vision_image_topic = '/demo_rgb/image'
- self.get_logger().info(f' Using fallback vision topic: {self.vision_image_topic}')
- if not self.grasp_pose_goal_topic:
- self.grasp_pose_goal_topic = '/demo_grasp/pose_goal'
- self.get_logger().info(f' Using fallback grasp input topic: {self.grasp_pose_goal_topic}')
- if not self.grasp_status_topic:
- self.grasp_status_topic = '/demo_grasp/pose_status'
- self.get_logger().info(f' Using fallback grasp output topic: {self.grasp_status_topic}')
-
- def start_callback(self, msg):
- """Handle skill start request from start_topic (JSON format)."""
- try:
- data = json.loads(msg.data)
- skill_id = data.get('skill_id', 'unknown')
- params = data.get('params', {})
- target_label = params.get('target_label', '')
-
- self.get_logger().info(f'Received pick request: skill_id={skill_id}, target_label={target_label}')
-
- if self.picking_in_progress:
- self.get_logger().warn('Pick operation already in progress, ignoring request')
- self._publish_status(skill_id, 'error', {'error': 'Operation already in progress'})
- return
-
- # Check if required primitives are available
- if not self.vision_image_topic or not self.grasp_pose_goal_topic:
- self.get_logger().error('Required primitives not available!')
- self._publish_status(skill_id, 'error', {'error': 'Required primitives not available'})
- return
-
- self.current_skill_id = skill_id
- self.current_target_label = target_label
- self.picking_in_progress = True
- self.grasp_complete = False
-
- # Publish running status
- self._publish_status(skill_id, 'running', {})
-
- # Step 1: Wait for image from vision primitive
- self.get_logger().info('Step 1: Waiting for image from prm::camera.capture...')
- # In real implementation, we would process the image here
- # For demo, we simulate object detection
-
- # Step 2: Simulate finding object and calculate pose
- time.sleep(0.2)
- self.get_logger().info(f'Step 2: Simulated detection of object with label: {target_label}')
-
- # Step 3: Send pose goal to grasp primitive
- pose_goal = PoseStamped()
- pose_goal.header.stamp = self.get_clock().now().to_msg()
- pose_goal.header.frame_id = 'base_frame'
- # Simulated pose for the object
- pose_goal.pose.position.x = 0.5
- pose_goal.pose.position.y = 0.2
- pose_goal.pose.position.z = 0.1
- pose_goal.pose.orientation.w = 1.0
-
- self.get_logger().info('Step 3: Sending pose goal to prm::arm.move.ee...')
- if self.pose_goal_publisher:
- self.pose_goal_publisher.publish(pose_goal)
- else:
- self.get_logger().error('Grasp primitive not available!')
- self._publish_status(skill_id, 'error', {'error': 'Grasp primitive not available'})
- self.picking_in_progress = False
-
- # Wait for grasp to complete (handled in grasp_status_callback)
-
- except json.JSONDecodeError as e:
- self.get_logger().error(f'Failed to parse start message JSON: {e}')
- self._publish_status('unknown', 'error', {'error': f'Invalid JSON: {e}'})
- except Exception as e:
- self.get_logger().error(f'Error in start_callback: {e}')
- import traceback
- self.get_logger().error(f'Traceback:\n{traceback.format_exc()}')
-
- def image_callback(self, msg):
- """Handle image from vision primitive (dynamic topic)."""
- self.latest_image = msg
- self.get_logger().debug(f'Received image from {self.vision_image_topic}')
-
- def grasp_status_callback(self, msg):
- """Handle status from grasp primitive (dynamic topic)."""
- # Parse status (could be JSON or Bool, depending on implementation)
- try:
- if hasattr(msg, 'data'):
- # Bool message
- success = msg.data
- else:
- # String message (JSON)
- status_data = json.loads(msg.data) if isinstance(msg.data, str) else msg.data
- success = status_data.get('success', False) if isinstance(status_data, dict) else bool(status_data)
- except:
- success = False
-
- if success and self.picking_in_progress:
- self.grasp_complete = True
- self.get_logger().info('Step 4: Grasp movement completed')
-
- # Publish skill success status
- result = {
- 'target_label': self.current_target_label,
- 'success': True
- }
- self._publish_status(self.current_skill_id, 'finished', result)
- self.get_logger().info('Pick skill completed successfully')
-
- self.picking_in_progress = False
- self.grasp_complete = False
- self.current_skill_id = None
- self.current_target_label = None
-
- def _publish_status(self, skill_id, state, result):
- """Publish skill status to status_topic (JSON format)."""
- status_msg = {
- 'skill_id': skill_id,
- 'state': state,
- 'result': result,
- 'diagnostics': {}
- }
-
- msg = String()
- msg.data = json.dumps(status_msg)
- self.status_publisher.publish(msg)
- self.get_logger().info(f'Published status: skill_id={skill_id}, state={state}')
-
-
-def main(args=None):
- rclpy.init(args=args)
- pick_skill = PickSkill()
-
- # Flag to track if shutdown was requested
- shutdown_requested = False
-
- def signal_handler(signum, frame):
- """Handle shutdown signals (SIGTERM, SIGINT)."""
- nonlocal shutdown_requested
- shutdown_requested = True
- pick_skill.get_logger().info(f'Received signal {signum}, shutting down...')
- # Shutdown rclpy to exit spin loop
- rclpy.shutdown()
-
- # Register signal handlers
- signal.signal(signal.SIGTERM, signal_handler)
- signal.signal(signal.SIGINT, signal_handler)
-
- try:
- rclpy.spin(pick_skill)
- except KeyboardInterrupt:
- shutdown_requested = True
- pick_skill.get_logger().info('Received KeyboardInterrupt, shutting down...')
- finally:
- if pick_skill.robonix_client:
- pick_skill.robonix_client.shutdown()
- pick_skill.destroy_node()
- rclpy.shutdown()
- if shutdown_requested:
- pick_skill.get_logger().info('Pick skill shutdown complete')
-
-
-if __name__ == '__main__':
- main()
diff --git a/rust/provider/demo_package/demo_rgb_provider/rgb_publisher.py b/rust/provider/demo_package/demo_rgb_provider/rgb_publisher.py
deleted file mode 100644
index 2f45498..0000000
--- a/rust/provider/demo_package/demo_rgb_provider/rgb_publisher.py
+++ /dev/null
@@ -1,76 +0,0 @@
-#!/usr/bin/env python3
-# SPDX-License-Identifier: MulanPSL-2.0
-# RGB Publisher
-#
-# Demo RGB camera package that publishes random color images.
-""""""
-
-import rclpy
-from rclpy.node import Node
-from sensor_msgs.msg import Image
-import numpy as np
-import random
-import time
-
-
-class RGBPublisher(Node):
- """Publishes random color images to simulate an RGB camera."""
-
- def __init__(self):
- super().__init__('demo_rgb_provider')
- self.publisher_ = self.create_publisher(Image, '/demo_rgb/image', 10)
- timer_period = 0.1 # 10 Hz
- self.timer = self.create_timer(timer_period, self.timer_callback)
- self.get_logger().info('Demo RGB camera package started. Publishing to /demo_rgb/image')
-
- def timer_callback(self):
- """Generate and publish a random color image."""
- msg = Image()
- msg.header.stamp = self.get_clock().now().to_msg()
- msg.header.frame_id = 'camera_frame'
-
- # Image dimensions: 640x480 RGB
- width = 640
- height = 480
- channels = 3
-
- # Generate random color (same color for entire image for simplicity)
- r = random.randint(0, 255)
- g = random.randint(0, 255)
- b = random.randint(0, 255)
-
- # Create image data (row-major order)
- msg.width = width
- msg.height = height
- msg.encoding = 'rgb8'
- msg.is_bigendian = False
- msg.step = width * channels # bytes per row
-
- # Fill image with random color
- image_data = np.zeros((height, width, channels), dtype=np.uint8)
- image_data[:, :, 0] = r # Red channel
- image_data[:, :, 1] = g # Green channel
- image_data[:, :, 2] = b # Blue channel
-
- msg.data = image_data.tobytes()
-
- self.publisher_.publish(msg)
- self.get_logger().debug(f'Published RGB image: R={r}, G={g}, B={b}')
-
-
-def main(args=None):
- rclpy.init(args=args)
- rgb_publisher = RGBPublisher()
-
- try:
- rclpy.spin(rgb_publisher)
- except KeyboardInterrupt:
- pass
- finally:
- rgb_publisher.destroy_node()
- rclpy.shutdown()
-
-
-if __name__ == '__main__':
- main()
-
diff --git a/rust/provider/demo_package/rbnx/build.sh b/rust/provider/demo_package/rbnx/build.sh
deleted file mode 100755
index b69a742..0000000
--- a/rust/provider/demo_package/rbnx/build.sh
+++ /dev/null
@@ -1,67 +0,0 @@
-#!/bin/bash
-# SPDX-License-Identifier: MulanPSL-2.0
-# Build Demo RGB Provider Package Script
-#
-# Build script for demo_rgb_provider package
-# This script is executed by 'rbnx deploy build' command
-# It should compile, install dependencies, or perform any necessary build steps
-
-set -e # Exit on error
-
-echo "Building demo_rgb_provider package..."
-
-SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-PACKAGE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
-cd "$PACKAGE_DIR"
-
-# Source ROS2 setup if available
-if [ -f /opt/ros/humble/setup.bash ]; then
- source /opt/ros/humble/setup.bash
-fi
-
-# Fix conda environment issues
-if [ -n "$CONDA_PREFIX" ]; then
- export LD_LIBRARY_PATH="/usr/lib/x86_64-linux-gnu:/lib/x86_64-linux-gnu:${LD_LIBRARY_PATH}"
- export LD_LIBRARY_PATH=$(echo "$LD_LIBRARY_PATH" | tr ':' '\n' | grep -v "$CONDA_PREFIX/lib" | tr '\n' ':' | sed 's/:$//')
- unset CONDA_DEFAULT_ENV
- unset CONDA_PREFIX
- unset CONDA_PROMPT_MODIFIER
- unset CONDA_PYTHON_EXE
- unset CONDA_SHLVL
- export PATH=$(echo $PATH | tr ':' '\n' | grep -v conda | tr '\n' ':' | sed 's/:$//')
-fi
-
-# Use system Python explicitly
-export PYTHON3_EXECUTABLE=/usr/bin/python3
-export PYTHON_EXECUTABLE=/usr/bin/python3
-
-# Find robonix-sdk directory
-ROBONIX_SDK_DIR=""
-if [ -n "$ROBONIX_SDK_PATH" ] && [ -d "$ROBONIX_SDK_PATH" ]; then
- ROBONIX_SDK_DIR="$ROBONIX_SDK_PATH"
-else
- # Search upward from package directory for robonix-sdk
- SEARCH_DIR="$PACKAGE_DIR"
- while [ "$SEARCH_DIR" != "/" ]; do
- if [ -d "$SEARCH_DIR/robonix-sdk" ]; then
- ROBONIX_SDK_DIR="$SEARCH_DIR/robonix-sdk"
- break
- fi
- SEARCH_DIR="$(dirname "$SEARCH_DIR")"
- done
-fi
-
-# Build demo_rgb_provider package using colcon
-echo "Building demo_rgb_provider package with colcon..."
-if command -v colcon > /dev/null 2>&1; then
- colcon build --packages-select demo_rgb_provider \
- --cmake-args \
- -DPYTHON3_EXECUTABLE=/usr/bin/python3 \
- -DCMAKE_PREFIX_PATH=/opt/ros/humble
- echo "Package built successfully!"
-else
- echo "Error: colcon not found. Please install colcon-common-extensions."
- exit 1
-fi
-
-echo "Build completed successfully!"
diff --git a/rust/provider/demo_package/rbnx/start_capture_rgb.sh b/rust/provider/demo_package/rbnx/start_capture_rgb.sh
deleted file mode 100755
index 71f5e49..0000000
--- a/rust/provider/demo_package/rbnx/start_capture_rgb.sh
+++ /dev/null
@@ -1,107 +0,0 @@
-#!/bin/bash
-# SPDX-License-Identifier: MulanPSL-2.0
-# Start Capture RGB Capability Script
-#
-# Start script for cap::vision.capture_rgb
-
-set -e
-
-SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-PACKAGE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
-cd "$PACKAGE_DIR"
-
-# Source ROS2 setup if available
-if [ -f /opt/ros/humble/setup.bash ]; then
- source /opt/ros/humble/setup.bash
-fi
-
-# Fix library path issues with conda environment
-if [ -n "$CONDA_PREFIX" ]; then
- export LD_LIBRARY_PATH="/usr/lib/x86_64-linux-gnu:/lib/x86_64-linux-gnu:${LD_LIBRARY_PATH}"
- export LD_LIBRARY_PATH=$(echo "$LD_LIBRARY_PATH" | tr ':' '\n' | grep -v "$CONDA_PREFIX/lib" | tr '\n' ':' | sed 's/:$//')
-fi
-
-# Check if package needs to be built
-if [ -f "package.xml" ] && [ ! -d "install" ]; then
- echo "Package not built, building now..."
- if command -v colcon > /dev/null 2>&1; then
- colcon build --packages-select demo_rgb_provider 2>&1 | tail -20
- if [ $? -ne 0 ]; then
- echo "Build failed, continuing anyway..."
- fi
- else
- echo "Warning: colcon not found, cannot build package"
- fi
-fi
-
-# Setup Python path - add both source and install directories
-SYSTEM_PYTHON="/usr/bin/python3"
-if [ -f "$SYSTEM_PYTHON" ]; then
- # Add source directory to PYTHONPATH (for development)
- if [ -d "$PACKAGE_DIR/demo_rgb_provider" ]; then
- export PYTHONPATH="$PACKAGE_DIR:${PYTHONPATH}"
- fi
- # Add install directory to PYTHONPATH (for installed package)
- if [ -d "$PACKAGE_DIR/install/demo_rgb_provider/lib/python3.10/site-packages" ]; then
- export PYTHONPATH="$PACKAGE_DIR/install/demo_rgb_provider/lib/python3.10/site-packages:${PYTHONPATH}"
- fi
- PYTHON_CMD="$SYSTEM_PYTHON"
- LD_LIBRARY_PATH="/usr/lib/x86_64-linux-gnu:/lib/x86_64-linux-gnu:$LD_LIBRARY_PATH"
-else
- PYTHON_CMD="python3"
-fi
-
-# Source robonix-sdk setup AFTER setting PYTHONPATH to ensure it's preserved
-# First try environment variable, then search upward from current directory
-ROBONIX_SDK_DIR=""
-if [ -n "$ROBONIX_SDK_PATH" ] && [ -f "$ROBONIX_SDK_PATH/install/setup.bash" ]; then
- ROBONIX_SDK_DIR="$ROBONIX_SDK_PATH"
-else
- # Search upward from package directory for robonix-sdk
- SEARCH_DIR="$PACKAGE_DIR"
- while [ "$SEARCH_DIR" != "/" ]; do
- if [ -d "$SEARCH_DIR/robonix-sdk" ] && [ -f "$SEARCH_DIR/robonix-sdk/install/setup.bash" ]; then
- ROBONIX_SDK_DIR="$SEARCH_DIR/robonix-sdk"
- break
- fi
- SEARCH_DIR="$(dirname "$SEARCH_DIR")"
- done
-fi
-
-# Set COLCON_CURRENT_PREFIX to current package directory to fix setup script path issues
-export COLCON_CURRENT_PREFIX="$PACKAGE_DIR"
-
-# Source the local setup if available (but don't fail if it doesn't exist)
-if [ -f "install/setup.bash" ]; then
- # Suppress the error about build time path
- source install/setup.bash 2>/dev/null || true
-elif [ -f "install/setup.sh" ]; then
- source install/setup.sh 2>/dev/null || true
-fi
-
-# Source robonix-sdk setup AFTER local setup to ensure robonixpy is in PYTHONPATH
-if [ -n "$ROBONIX_SDK_DIR" ] && [ -f "$ROBONIX_SDK_DIR/install/setup.bash" ]; then
- # Source setup.bash which will add robonixpy to PYTHONPATH
- # Save current PYTHONPATH, source, then restore to ensure robonixpy is included
- OLD_PYTHONPATH="$PYTHONPATH"
- if source "$ROBONIX_SDK_DIR/install/setup.bash" 2>&1; then
- # Merge PYTHONPATH: robonix paths first, then old paths
- export PYTHONPATH="$PYTHONPATH:$OLD_PYTHONPATH"
- echo "[INFO] Sourced robonix-sdk setup.bash, PYTHONPATH includes robonixpy" >&2
- else
- echo "[WARN] Failed to source robonix-sdk setup.bash" >&2
- export PYTHONPATH="$OLD_PYTHONPATH"
- fi
-else
- echo "[WARN] robonix-sdk not found, robonixpy may not be available" >&2
-fi
-
-# Start RGB publisher (capability: cap::vision.capture_rgb)
-# Try ros2 run first, fallback to Python module if not available
-if command -v ros2 > /dev/null 2>&1 && ros2 pkg list 2>/dev/null | grep -q "^demo_rgb_provider$"; then
- exec ros2 run demo_rgb_provider rgb_publisher
-else
- # Use Python module directly
- exec $PYTHON_CMD -m demo_rgb_provider.rgb_publisher
-fi
-
diff --git a/rust/provider/demo_package/rbnx/start_grasp_move.sh b/rust/provider/demo_package/rbnx/start_grasp_move.sh
deleted file mode 100755
index 7a534a9..0000000
--- a/rust/provider/demo_package/rbnx/start_grasp_move.sh
+++ /dev/null
@@ -1,107 +0,0 @@
-#!/bin/bash
-# SPDX-License-Identifier: MulanPSL-2.0
-# Start Grasp Move Capability Script
-#
-# Start script for cap::grasp.move
-
-set -e
-
-SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-PACKAGE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
-cd "$PACKAGE_DIR"
-
-# Source ROS2 setup if available
-if [ -f /opt/ros/humble/setup.bash ]; then
- source /opt/ros/humble/setup.bash
-fi
-
-# Fix library path issues with conda environment
-if [ -n "$CONDA_PREFIX" ]; then
- export LD_LIBRARY_PATH="/usr/lib/x86_64-linux-gnu:/lib/x86_64-linux-gnu:${LD_LIBRARY_PATH}"
- export LD_LIBRARY_PATH=$(echo "$LD_LIBRARY_PATH" | tr ':' '\n' | grep -v "$CONDA_PREFIX/lib" | tr '\n' ':' | sed 's/:$//')
-fi
-
-# Check if package needs to be built
-if [ -f "package.xml" ] && [ ! -d "install" ]; then
- echo "Package not built, building now..."
- if command -v colcon > /dev/null 2>&1; then
- colcon build --packages-select demo_rgb_provider 2>&1 | tail -20
- if [ $? -ne 0 ]; then
- echo "Build failed, continuing anyway..."
- fi
- else
- echo "Warning: colcon not found, cannot build package"
- fi
-fi
-
-# Setup Python path - add both source and install directories
-SYSTEM_PYTHON="/usr/bin/python3"
-if [ -f "$SYSTEM_PYTHON" ]; then
- # Add source directory to PYTHONPATH (for development)
- if [ -d "$PACKAGE_DIR/demo_rgb_provider" ]; then
- export PYTHONPATH="$PACKAGE_DIR:${PYTHONPATH}"
- fi
- # Add install directory to PYTHONPATH (for installed package)
- if [ -d "$PACKAGE_DIR/install/demo_rgb_provider/lib/python3.10/site-packages" ]; then
- export PYTHONPATH="$PACKAGE_DIR/install/demo_rgb_provider/lib/python3.10/site-packages:${PYTHONPATH}"
- fi
- PYTHON_CMD="$SYSTEM_PYTHON"
- LD_LIBRARY_PATH="/usr/lib/x86_64-linux-gnu:/lib/x86_64-linux-gnu:$LD_LIBRARY_PATH"
-else
- PYTHON_CMD="python3"
-fi
-
-# Source robonix-sdk setup AFTER setting PYTHONPATH to ensure it's preserved
-# First try environment variable, then search upward from current directory
-ROBONIX_SDK_DIR=""
-if [ -n "$ROBONIX_SDK_PATH" ] && [ -f "$ROBONIX_SDK_PATH/install/setup.bash" ]; then
- ROBONIX_SDK_DIR="$ROBONIX_SDK_PATH"
-else
- # Search upward from package directory for robonix-sdk
- SEARCH_DIR="$PACKAGE_DIR"
- while [ "$SEARCH_DIR" != "/" ]; do
- if [ -d "$SEARCH_DIR/robonix-sdk" ] && [ -f "$SEARCH_DIR/robonix-sdk/install/setup.bash" ]; then
- ROBONIX_SDK_DIR="$SEARCH_DIR/robonix-sdk"
- break
- fi
- SEARCH_DIR="$(dirname "$SEARCH_DIR")"
- done
-fi
-
-# Set COLCON_CURRENT_PREFIX to current package directory to fix setup script path issues
-export COLCON_CURRENT_PREFIX="$PACKAGE_DIR"
-
-# Source the local setup if available (but don't fail if it doesn't exist)
-if [ -f "install/setup.bash" ]; then
- # Suppress the error about build time path
- source install/setup.bash 2>/dev/null || true
-elif [ -f "install/setup.sh" ]; then
- source install/setup.sh 2>/dev/null || true
-fi
-
-# Source robonix-sdk setup AFTER local setup to ensure robonixpy is in PYTHONPATH
-if [ -n "$ROBONIX_SDK_DIR" ] && [ -f "$ROBONIX_SDK_DIR/install/setup.bash" ]; then
- # Source setup.bash which will add robonixpy to PYTHONPATH
- # Save current PYTHONPATH, source, then restore to ensure robonixpy is included
- OLD_PYTHONPATH="$PYTHONPATH"
- if source "$ROBONIX_SDK_DIR/install/setup.bash" 2>&1; then
- # Merge PYTHONPATH: robonix paths first, then old paths
- export PYTHONPATH="$PYTHONPATH:$OLD_PYTHONPATH"
- echo "[INFO] Sourced robonix-sdk setup.bash, PYTHONPATH includes robonixpy" >&2
- else
- echo "[WARN] Failed to source robonix-sdk setup.bash" >&2
- export PYTHONPATH="$OLD_PYTHONPATH"
- fi
-else
- echo "[WARN] robonix-sdk not found, robonixpy may not be available" >&2
-fi
-
-# Start grasp move (capability: cap::grasp.move)
-# Try ros2 run first, fallback to Python module if not available
-if command -v ros2 > /dev/null 2>&1 && ros2 pkg list 2>/dev/null | grep -q "^demo_rgb_provider$"; then
- exec ros2 run demo_rgb_provider grasp_move
-else
- # Use Python module directly
- exec $PYTHON_CMD -m demo_rgb_provider.grasp_move
-fi
-
diff --git a/rust/provider/demo_package/rbnx/start_pick.sh b/rust/provider/demo_package/rbnx/start_pick.sh
deleted file mode 100755
index 0420f2a..0000000
--- a/rust/provider/demo_package/rbnx/start_pick.sh
+++ /dev/null
@@ -1,124 +0,0 @@
-#!/bin/bash
-# SPDX-License-Identifier: MulanPSL-2.0
-# Start Pick Skill Script
-#
-# Start script for skl::pick
-
-set -e
-
-SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-PACKAGE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
-cd "$PACKAGE_DIR"
-
-# Source ROS2 setup if available
-if [ -f /opt/ros/humble/setup.bash ]; then
- source /opt/ros/humble/setup.bash
-fi
-
-# Source robonix-sdk setup to make robonixpy SDK available
-# First try environment variable, then search upward from current directory
-ROBONIX_SDK_DIR=""
-if [ -n "$ROBONIX_SDK_PATH" ] && [ -f "$ROBONIX_SDK_PATH/install/setup.bash" ]; then
- ROBONIX_SDK_DIR="$ROBONIX_SDK_PATH"
-else
- # Search upward from package directory for robonix-sdk
- SEARCH_DIR="$PACKAGE_DIR"
- while [ "$SEARCH_DIR" != "/" ]; do
- if [ -d "$SEARCH_DIR/robonix-sdk" ] && [ -f "$SEARCH_DIR/robonix-sdk/install/setup.bash" ]; then
- ROBONIX_SDK_DIR="$SEARCH_DIR/robonix-sdk"
- break
- fi
- SEARCH_DIR="$(dirname "$SEARCH_DIR")"
- done
-fi
-
-# Fix library path issues with conda environment
-if [ -n "$CONDA_PREFIX" ]; then
- export LD_LIBRARY_PATH="/usr/lib/x86_64-linux-gnu:/lib/x86_64-linux-gnu:${LD_LIBRARY_PATH}"
- export LD_LIBRARY_PATH=$(echo "$LD_LIBRARY_PATH" | tr ':' '\n' | grep -v "$CONDA_PREFIX/lib" | tr '\n' ':' | sed 's/:$//')
-fi
-
-# Check if package needs to be built
-if [ -f "package.xml" ] && [ ! -d "install" ]; then
- echo "Package not built, building now..."
- if command -v colcon > /dev/null 2>&1; then
- colcon build --packages-select demo_rgb_provider 2>&1 | tail -20
- if [ $? -ne 0 ]; then
- echo "Build failed, continuing anyway..."
- fi
- else
- echo "Warning: colcon not found, cannot build package"
- fi
-fi
-
-# Setup Python path - add both source and install directories
-SYSTEM_PYTHON="/usr/bin/python3"
-if [ -f "$SYSTEM_PYTHON" ]; then
- # Add source directory to PYTHONPATH (for development)
- if [ -d "$PACKAGE_DIR/demo_rgb_provider" ]; then
- export PYTHONPATH="$PACKAGE_DIR:${PYTHONPATH}"
- fi
- # Add install directory to PYTHONPATH (for installed package)
- if [ -d "$PACKAGE_DIR/install/demo_rgb_provider/lib/python3.10/site-packages" ]; then
- export PYTHONPATH="$PACKAGE_DIR/install/demo_rgb_provider/lib/python3.10/site-packages:${PYTHONPATH}"
- fi
- PYTHON_CMD="$SYSTEM_PYTHON"
- LD_LIBRARY_PATH="/usr/lib/x86_64-linux-gnu:/lib/x86_64-linux-gnu:$LD_LIBRARY_PATH"
-else
- PYTHON_CMD="python3"
-fi
-
-# Source robonix-sdk setup AFTER setting PYTHONPATH to ensure it's preserved
-# First try environment variable, then search upward from current directory
-ROBONIX_SDK_DIR=""
-if [ -n "$ROBONIX_SDK_PATH" ] && [ -f "$ROBONIX_SDK_PATH/install/setup.bash" ]; then
- ROBONIX_SDK_DIR="$ROBONIX_SDK_PATH"
-else
- # Search upward from package directory for robonix-sdk
- SEARCH_DIR="$PACKAGE_DIR"
- while [ "$SEARCH_DIR" != "/" ]; do
- if [ -d "$SEARCH_DIR/robonix-sdk" ] && [ -f "$SEARCH_DIR/robonix-sdk/install/setup.bash" ]; then
- ROBONIX_SDK_DIR="$SEARCH_DIR/robonix-sdk"
- break
- fi
- SEARCH_DIR="$(dirname "$SEARCH_DIR")"
- done
-fi
-
-# Set COLCON_CURRENT_PREFIX to current package directory to fix setup script path issues
-export COLCON_CURRENT_PREFIX="$PACKAGE_DIR"
-
-# Source the local setup if available (but don't fail if it doesn't exist)
-if [ -f "install/setup.bash" ]; then
- # Suppress the error about build time path
- source install/setup.bash 2>/dev/null || true
-elif [ -f "install/setup.sh" ]; then
- source install/setup.sh 2>/dev/null || true
-fi
-
-# Source robonix-sdk setup AFTER local setup to ensure robonixpy is in PYTHONPATH
-if [ -n "$ROBONIX_SDK_DIR" ] && [ -f "$ROBONIX_SDK_DIR/install/setup.bash" ]; then
- # Source setup.bash which will add robonixpy to PYTHONPATH
- # Save current PYTHONPATH, source, then restore to ensure robonixpy is included
- OLD_PYTHONPATH="$PYTHONPATH"
- if source "$ROBONIX_SDK_DIR/install/setup.bash" 2>&1; then
- # Merge PYTHONPATH: robonix paths first, then old paths
- export PYTHONPATH="$PYTHONPATH:$OLD_PYTHONPATH"
- echo "[INFO] Sourced robonix-sdk setup.bash, PYTHONPATH includes robonixpy" >&2
- else
- echo "[WARN] Failed to source robonix-sdk setup.bash" >&2
- export PYTHONPATH="$OLD_PYTHONPATH"
- fi
-else
- echo "[WARN] robonix-sdk not found, robonixpy may not be available" >&2
-fi
-
-# Start pick skill (skill: skl::pick)
-# Try ros2 run first, fallback to Python module if not available
-if command -v ros2 > /dev/null 2>&1 && ros2 pkg list 2>/dev/null | grep -q "^demo_rgb_provider$"; then
- exec ros2 run demo_rgb_provider pick_skill
-else
- # Use Python module directly
- exec $PYTHON_CMD -m demo_rgb_provider.pick_skill
-fi
-
diff --git a/rust/provider/demo_package/rbnx/stop_capture_rgb.sh b/rust/provider/demo_package/rbnx/stop_capture_rgb.sh
deleted file mode 100755
index 3db103b..0000000
--- a/rust/provider/demo_package/rbnx/stop_capture_rgb.sh
+++ /dev/null
@@ -1,17 +0,0 @@
-#!/bin/bash
-# SPDX-License-Identifier: MulanPSL-2.0
-# Stop Capture RGB Capability Script
-#
-# Stop script for cap::vision.capture_rgb
-# This script is called to stop the RGB publisher process
-# CLI will also manage the process by PID, but this script can be used
-# for additional cleanup if needed
-
-# Kill by process name as fallback
-pkill -f "rgb_publisher" || true
-
-# Clean up any temporary files
-rm -f /tmp/demo_rgb_cap.log
-
-exit 0
-
diff --git a/rust/provider/demo_package/rbnx/stop_grasp_move.sh b/rust/provider/demo_package/rbnx/stop_grasp_move.sh
deleted file mode 100755
index d801c1c..0000000
--- a/rust/provider/demo_package/rbnx/stop_grasp_move.sh
+++ /dev/null
@@ -1,17 +0,0 @@
-#!/bin/bash
-# SPDX-License-Identifier: MulanPSL-2.0
-# Stop Grasp Move Capability Script
-#
-# Stop script for cap::grasp.move
-# This script is called to stop the grasp move process
-# CLI will also manage the process by PID, but this script can be used
-# for additional cleanup if needed
-
-# Kill by process name as fallback
-pkill -f "grasp_move" || true
-
-# Clean up any temporary files
-rm -f /tmp/demo_grasp_cap.log
-
-exit 0
-
diff --git a/rust/provider/demo_package/rbnx/stop_pick.sh b/rust/provider/demo_package/rbnx/stop_pick.sh
deleted file mode 100755
index 2f7331f..0000000
--- a/rust/provider/demo_package/rbnx/stop_pick.sh
+++ /dev/null
@@ -1,17 +0,0 @@
-#!/bin/bash
-# SPDX-License-Identifier: MulanPSL-2.0
-# Stop Pick Skill Script
-#
-# Stop script for skl::pick
-# This script is called to stop the pick skill process
-# CLI will also manage the process by PID, but this script can be used
-# for additional cleanup if needed
-
-# Kill by process name as fallback
-pkill -f "pick_skill" || true
-
-# Clean up any temporary files
-rm -f /tmp/demo_pick_skill.log
-
-exit 0
-
diff --git a/rust/provider/demo_package/rbnx_manifest.yaml b/rust/provider/demo_package/rbnx_manifest.yaml
deleted file mode 100644
index bafb236..0000000
--- a/rust/provider/demo_package/rbnx_manifest.yaml
+++ /dev/null
@@ -1,55 +0,0 @@
-# Robonix Package Manifest
-# This file defines the primitives, services, and skills provided by this package
-# Format version: 2.0 (EAIOS Architecture)
-#
-# Note:
-# - Primitives and services must conform to standard specifications
-# - Skills are flexible and user-defined
-# - JSON fields (input_schema, output_schema, metadata, start_args, status) must be valid JSON strings
-
-package:
- name: demo_rgb_provider
- version: 0.0.1
- description: Demo package with primitives and skills
- maintainer: root
- maintainer_email: demo@demo.demo
- license: MulanPSL-2.0
- # build_script: rbnx/build.sh # Optional: build script path. If omitted, defaults to rbnx/build.sh
-
-# Primitives provided by this package
-# Primitives provide standardized hardware capability mapping
-primitives:
- - name: prm::camera.capture
- # Spec defines: OUTPUT: {"image": "sensor_msgs/Image"}
- input_schema: '{}'
- output_schema: '{"image":"/demo_rgb/image"}'
- metadata: '{"resolution":"640x480","format":"RGB"}'
- version: 0.0.1-impl-alpha
- start_script: rbnx/start_capture_rgb.sh
- stop_script: rbnx/stop_capture_rgb.sh
-
- - name: prm::arm.move.ee
- # Spec defines:
- # INPUT: {"pose": "geometry_msgs/PoseStamped"}
- # OUTPUT: {"status": "bool"}
- input_schema: '{"pose":"/demo_grasp/pose_goal"}'
- output_schema: '{"status":"/demo_grasp/pose_status"}'
- metadata: '{"robot":"demo_arm"}'
- version: 0.0.1
- start_script: rbnx/start_grasp_move.sh
- stop_script: rbnx/stop_grasp_move.sh
-
-# Skills provided by this package
-# Skills are user-defined and flexible, can be basic (static program) or rtdl (RTDL-based)
-skills:
- - name: skl::pick
- type: basic
- start_topic: /robot1/skill/pick/start
- status_topic: /robot1/skill/pick/status
- entry: python3 /path/to/pick_skill.py
- start_args: '{"target_label":"string"}'
- status: '{"state":"string","result":"any"}'
- metadata: '{"domain":"demo","capability":["vision","manipulation"]}'
- version: 0.0.1
- start_script: rbnx/start_pick.sh
- stop_script: rbnx/stop_pick.sh
diff --git a/rust/provider/demo_package/setup.cfg b/rust/provider/demo_package/setup.cfg
deleted file mode 100644
index b2197ba..0000000
--- a/rust/provider/demo_package/setup.cfg
+++ /dev/null
@@ -1,5 +0,0 @@
-[develop]
-script_dir=$base/lib/demo_rgb_provider
-[install]
-install_scripts=$base/lib/demo_rgb_provider
-
diff --git a/rust/provider/demo_package/setup.py b/rust/provider/demo_package/setup.py
deleted file mode 100644
index ba36cfd..0000000
--- a/rust/provider/demo_package/setup.py
+++ /dev/null
@@ -1,34 +0,0 @@
-# SPDX-License-Identifier: MulanPSL-2.0
-# Setup Script for Demo RGB Provider Package
-#
-# Setup script for demo_rgb_provider ROS2 package
-
-from setuptools import find_packages, setup
-
-package_name = 'demo_rgb_provider'
-
-setup(
- name=package_name,
- version='0.0.1',
- packages=find_packages(exclude=['test']),
- data_files=[
- ('share/ament_index/resource_index/packages',
- ['resource/' + package_name]),
- ('share/' + package_name, ['package.xml']),
- ],
- install_requires=['setuptools', 'numpy'],
- zip_safe=True,
- maintainer='root',
- maintainer_email='demo@demo.demo',
- description='Demo RGB camera package that outputs random color images',
- license='MulanPSL-2.0',
- entry_points={
- 'console_scripts': [
- 'rgb_publisher = demo_rgb_provider.rgb_publisher:main',
- 'grasp_move = demo_rgb_provider.grasp_move:main',
- 'pick_skill = demo_rgb_provider.pick_skill:main',
- 'update_map_skill = demo_rgb_provider.update_map_skill:main',
- ],
- },
-)
-
diff --git a/rust/provider/demo_package_service/.env.example b/rust/provider/demo_package_service/.env.example
deleted file mode 100644
index 01d2a69..0000000
--- a/rust/provider/demo_package_service/.env.example
+++ /dev/null
@@ -1,9 +0,0 @@
-# SPDX-License-Identifier: MulanPSL-2.0
-# Environment Variables Configuration
-#
-# Copy this file to .env and fill in your DeepSeek API key
-#
-# Get your API key from: https://platform.deepseek.com/
-
-# DeepSeek API Key (required for task planning service)
-DEEPSEEK_API_KEY=your_deepseek_api_key_here
diff --git a/rust/provider/demo_package_service/README.md b/rust/provider/demo_package_service/README.md
deleted file mode 100644
index b097931..0000000
--- a/rust/provider/demo_package_service/README.md
+++ /dev/null
@@ -1,145 +0,0 @@
-# Demo Service Provider Package
-
-SPDX-License-Identifier: MulanPSL-2.0
-
-This package provides demo implementations of Robonix services:
-- **semantic_map**: Provides object-level representation of the environment
-- **task_plan**: Converts natural language task descriptions to RTDL code using DeepSeek LLM
-
-## Services
-
-### Semantic Map Service
-
-The semantic map service (`semantic_map_service`) simulates an object graph by constructing mock objects. It provides an object-level representation on top of spatial maps.
-
-**Service Interface**: `/demo_service/semantic_map/query`
-**Service Type**: `robonix_sdk/srv/service/semantic_map/QuerySemanticMap`
-
-### Task Plan Service
-
-The task plan service (`task_plan_service`) converts natural language task descriptions into RTDL (Real-Time Decision Logic) code. It uses DeepSeek LLM API for intelligent task planning.
-
-**Service Interface**: `/demo_service/task_plan/plan`
-**Service Type**: `robonix_sdk/srv/service/task_plan/PlanTask`
-
-## Configuration
-
-### DeepSeek API Configuration
-
-The task plan service requires a DeepSeek API key to function. Follow these steps to configure it:
-
-1. **Get a DeepSeek API Key**
- - Visit [DeepSeek Platform](https://platform.deepseek.com/)
- - Sign up or log in to your account
- - Navigate to API keys section
- - Create a new API key
-
-2. **Configure the API Key**
- - Copy the `.env.example` file to `.env` in this package directory:
- ```bash
- cp .env.example .env
- ```
- - Edit `.env` and replace `your_deepseek_api_key_here` with your actual API key:
- ```
- DEEPSEEK_API_KEY=sk-your-actual-api-key-here
- ```
- - Make sure `.env` file is in the package root directory (same level as `setup.py`)
-
-3. **Verify Configuration**
- - The service will automatically load the `.env` file on startup
- - If the API key is not found, the service will fall back to simple keyword-based planning
- - Check service logs to confirm DeepSeek API is initialized
-
-## Building and Installation
-
-### Prerequisites
-
-- ROS2 (Humble or later)
-- Python 3.8+
-- colcon build tools
-- DeepSeek API key (for task planning service)
-
-### Build
-
-```bash
-# From package directory
-colcon build --packages-select demo_service_provider
-```
-
-Or use robonix-cli:
-
-```bash
-rbnx deploy build
-```
-
-### Install Dependencies
-
-The package requires the following Python packages (automatically installed via setup.py):
-- `python-dotenv`: For loading environment variables from .env file
-- `openai`: For DeepSeek API client (compatible with DeepSeek API)
-
-## Usage
-
-### Start Services
-
-Services can be started individually or via robonix-cli:
-
-```bash
-# Start semantic map service
-rbnx deploy start semantic_map
-
-# Start task plan service
-rbnx deploy start task_plan
-
-# Or start all services
-rbnx deploy start all
-```
-
-### Service Endpoints
-
-- **Semantic Map**: `/demo_service/semantic_map/query`
-- **Task Plan**: `/demo_service/task_plan/plan`
-
-## RTDL Format
-
-The task plan service generates RTDL code in JSON format:
-
-```json
-[
- {
- "type": "skill",
- "name": "pick",
- "params": {
- "target": "cup_001"
- }
- },
- {
- "type": "skill",
- "name": "place",
- "params": {
- "destination": "table_001"
- }
- }
-]
-```
-
-## Troubleshooting
-
-### DeepSeek API Not Working
-
-1. Check that `.env` file exists and contains `DEEPSEEK_API_KEY`
-2. Verify the API key is correct and has sufficient credits
-3. Check service logs for API errors
-4. The service will automatically fall back to simple planning if API fails
-
-### Service Not Starting
-
-1. Ensure ROS2 is properly sourced: `source /opt/ros/humble/setup.bash`
-2. Check that robonix-sdk is built and sourced
-3. Verify Python dependencies are installed
-4. Check service logs in `rbnx/` directory
-
-## License
-
-MulanPSL-2.0
-
diff --git a/rust/provider/demo_package_service/demo_service_provider/__init__.py b/rust/provider/demo_package_service/demo_service_provider/__init__.py
deleted file mode 100644
index db3edee..0000000
--- a/rust/provider/demo_package_service/demo_service_provider/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-# SPDX-License-Identifier: MulanPSL-2.0
-# Demo Service Provider Package
-#
-# Python package initializer for demo_service_provider
diff --git a/rust/provider/demo_package_service/demo_service_provider/semantic_map_service.py b/rust/provider/demo_package_service/demo_service_provider/semantic_map_service.py
deleted file mode 100644
index 7d5bcf9..0000000
--- a/rust/provider/demo_package_service/demo_service_provider/semantic_map_service.py
+++ /dev/null
@@ -1,192 +0,0 @@
-#!/usr/bin/env python3
-# SPDX-License-Identifier: MulanPSL-2.0
-# Semantic Map Service
-#
-# Demo semantic map service implementation.
-# Simulates an object graph by constructing mock objects.
-""""""
-
-import rclpy
-from rclpy.node import Node
-from robonix_sdk.srv import QuerySemanticMap
-from robonix_sdk.msg import Object, Relation, RelationType, FrameMapping, Point3D, BoundingBox
-from builtin_interfaces.msg import Time
-import random
-import json
-
-
-class SemanticMapService(Node):
- """Implements semantic_map service with simulated object graph."""
-
- def __init__(self):
- super().__init__('demo_semantic_map_service')
-
- # Create service
- self.service = self.create_service(
- QuerySemanticMap,
- '/demo_service/semantic_map/query',
- self.query_callback
- )
-
- self.get_logger().info('Semantic map service started')
- self.get_logger().info(' Service: /demo_service/semantic_map/query')
-
- def query_callback(self, request, response):
- """Handle semantic map query request."""
- self.get_logger().info(f'Received query with types filter: {request.types}')
-
- # Generate mock object graph
- objects = self._generate_mock_objects(request.types)
-
- # Set response - objects is Object[] as per service definition
- response.objects = objects
- response.stamp = self.get_clock().now().to_msg()
-
- self.get_logger().info(f'Returning {len(objects)} objects')
-
- # Log objects for debugging
- objects_json = self._objects_to_json(objects)
- self.get_logger().info(f'Objects JSON: {json.dumps(objects_json, indent=2)}')
- return response
-
- def _objects_to_json(self, objects):
- """Convert Object messages to JSON-serializable format."""
- result = []
- for obj in objects:
- obj_dict = {
- 'id': obj.id,
- 'label': obj.label,
- 'registered_skills': list(obj.registered_skills),
- 'registered_primitives': list(obj.registered_primitives),
- 'relations': []
- }
-
- # Convert relations
- for rel in obj.relations:
- rel_dict = {
- 'relation_type': rel.relation_type.type if hasattr(rel.relation_type, 'type') else None,
- 'target_entity_id': rel.target_entity_id
- }
- obj_dict['relations'].append(rel_dict)
-
- # Convert frame mappings
- obj_dict['frame_mapping'] = []
- for fm in obj.frame_mapping:
- fm_dict = {
- 'frame_id': fm.frame_id,
- 'center': {
- 'x': fm.center.x,
- 'y': fm.center.y,
- 'z': fm.center.z
- },
- 'bbox': []
- }
- for bbox in fm.bbox:
- bbox_dict = {
- 'scale_x': bbox.scale_x,
- 'scale_y': bbox.scale_y,
- 'scale_z': bbox.scale_z,
- 'yaw': bbox.yaw
- }
- fm_dict['bbox'].append(bbox_dict)
- obj_dict['frame_mapping'].append(fm_dict)
-
- result.append(obj_dict)
- return result
-
- def _generate_mock_objects(self, type_filter=None):
- """Generate a mock object graph."""
- objects = []
-
- # Define mock objects - simplified and aligned with specs
- # Primitives from specs_table.rs: prm::camera.capture, prm::arm.move.ee, prm::gripper.close
- # Note: Only robots have skills and primitives. Objects in the environment don't have skills.
- mock_data = [
- {
- 'id': 'robot_001',
- 'label': 'robot1',
- 'type': 'robot',
- 'skills': ['skl::pick'], # should be queried from OS - TODO!
- 'primitives': ['prm::arm.move.ee', 'prm::gripper.close', 'prm::camera.capture'], # Standard primitives from specs
- 'position': (0.5, 0.5, 0.0),
- 'size': (0.4, 0.4, 0.8)
- },
- {
- 'id': 'table_001',
- 'label': 'dining_table',
- 'type': 'table',
- 'skills': [], # Objects don't have skills
- 'primitives': [],
- 'position': (1.0, 1.0, 0.4),
- 'size': (1.2, 0.8, 0.4)
- },
- {
- 'id': 'box_001',
- 'label': 'red_box',
- 'type': 'box',
- 'skills': [], # Objects don't have skills
- 'primitives': [],
- 'position': (1.0, 1.0, 0.8),
- 'size': (0.2, 0.2, 0.2),
- 'parent': 'table_001'
- }
- ]
-
- # Filter by type if specified
- if type_filter and len(type_filter) > 0:
- mock_data = [obj for obj in mock_data if obj['type'] in type_filter]
-
- # Convert to Object messages
- for data in mock_data:
- obj = Object()
- obj.id = data['id']
- obj.label = data['label']
-
- # Create relations
- obj.relations = []
- if 'parent' in data:
- relation = Relation()
- relation.relation_type = RelationType()
- relation.relation_type.type = RelationType.CHILD_OF
- relation.target_entity_id = data['parent']
- obj.relations.append(relation)
-
- # Set registered skills and primitives (aligned with Object.msg and specs_table.rs)
- obj.registered_skills = data['skills']
- obj.registered_primitives = data['primitives'] # Use standard primitive names from specs
-
- # Create frame mapping
- obj.frame_mapping = []
- frame_mapping = FrameMapping()
- frame_mapping.center = Point3D()
- frame_mapping.center.x = float(data['position'][0])
- frame_mapping.center.y = float(data['position'][1])
- frame_mapping.center.z = float(data['position'][2])
-
- # Create bounding box
- bbox = BoundingBox()
- bbox.scale_x = float(data['size'][0])
- bbox.scale_y = float(data['size'][1])
- bbox.scale_z = float(data['size'][2])
- bbox.yaw = 0.0
- frame_mapping.bbox = [bbox]
-
- frame_mapping.frame_id = 'map'
- obj.frame_mapping.append(frame_mapping)
-
- objects.append(obj)
-
- return objects
-
-
-def main(args=None):
- rclpy.init(args=args)
- semantic_map_service = SemanticMapService()
- rclpy.spin(semantic_map_service)
- semantic_map_service.destroy_node()
- rclpy.shutdown()
-
-
-if __name__ == '__main__':
- main()
-
diff --git a/rust/provider/demo_package_service/demo_service_provider/task_plan_service.py b/rust/provider/demo_package_service/demo_service_provider/task_plan_service.py
deleted file mode 100644
index 7c923fc..0000000
--- a/rust/provider/demo_package_service/demo_service_provider/task_plan_service.py
+++ /dev/null
@@ -1,340 +0,0 @@
-#!/usr/bin/env python3
-# SPDX-License-Identifier: MulanPSL-2.0
-# Task Plan Service
-#
-# Demo task plan service implementation.
-# Converts natural language task description to RTDL code.
-# Uses DeepSeek LLM API for intelligent task planning.
-# Uses simple list-style RTDL format (assembly-like instruction list).
-
-import os
-import json
-from pathlib import Path
-import rclpy
-from rclpy.node import Node
-from robonix_sdk.srv import PlanTask
-from builtin_interfaces.msg import Time
-from dotenv import load_dotenv
-from openai import OpenAI
-
-
-class TaskPlanService(Node):
- """Implements task_plan service with DeepSeek LLM for RTDL generation."""
-
- def __init__(self):
- super().__init__('demo_task_plan_service')
-
- # Load environment variables from .env file in package root directory
- # Find package root directory (containing setup.py) by walking up from current file
- current_file = Path(__file__).resolve()
- package_root = current_file.parent
- # Walk up directories to find package root (containing setup.py)
- while package_root != package_root.parent:
- if (package_root / 'setup.py').exists():
- break
- package_root = package_root.parent
- # If setup.py not found, use current file's parent's parent as fallback
- if not (package_root / 'setup.py').exists():
- package_root = current_file.parent.parent
-
- env_path = package_root / '.env'
- load_dotenv(env_path)
-
- # Get DeepSeek API key from environment
- self.api_key = os.getenv('DEEPSEEK_API_KEY')
- if not self.api_key:
- self.get_logger().error(
- 'DEEPSEEK_API_KEY not found in .env file. '
- 'Please configure DEEPSEEK_API_KEY in .env file. '
- 'See README.md for configuration instructions.'
- )
- raise ValueError(
- 'DEEPSEEK_API_KEY not found. '
- 'Please configure DEEPSEEK_API_KEY in .env file.'
- )
-
- # Validate API key format
- if not self._validate_api_key(self.api_key):
- self.get_logger().error(
- 'Invalid DEEPSEEK_API_KEY format. '
- 'API key should start with "sk-" and be at least 35 characters long. '
- 'Please check your .env file.'
- )
- raise ValueError(
- 'Invalid DEEPSEEK_API_KEY format. '
- 'API key should start with "sk-" and be at least 35 characters long.'
- )
-
- # Initialize DeepSeek client
- try:
- self.deepseek_client = OpenAI(
- base_url="https://api.deepseek.com",
- api_key=self.api_key,
- )
- self.get_logger().info(f'DeepSeek API client initialized with key: {self.api_key[:10]}...')
- except Exception as e:
- self.get_logger().error(f'Failed to initialize DeepSeek API client: {e}')
- raise
-
- # Create service
- self.service = self.create_service(
- PlanTask,
- '/demo_service/task_plan/plan',
- self.plan_callback
- )
-
- self.get_logger().info('Task plan service started')
- self.get_logger().info(' Service: /demo_service/task_plan/plan')
-
- def plan_callback(self, request, response):
- """Handle task planning request."""
- self.get_logger().info('=' * 80)
- self.get_logger().info('Task Plan Service: Received planning request')
- self.get_logger().info('=' * 80)
- self.get_logger().info(f'Task Description: {request.description}')
- self.get_logger().info(f'Params keys: {request.params.keys if request.params else []}')
-
- # Parse params to extract object graph, RTDL syntax, and skill/primitive specs
- object_graph = None
- rtdl_syntax = None
- skill_primitive_specs = None
-
- if request.params:
- # Extract all params from Dict (keys and values arrays)
- for i, key in enumerate(request.params.keys):
- if i < len(request.params.values):
- try:
- if key == 'object_graph':
- object_graph = json.loads(request.params.values[i])
- obj_count = len(object_graph) if isinstance(object_graph, list) else "unknown"
- self.get_logger().info(f'Object Graph: Found {obj_count} objects')
- if object_graph:
- self.get_logger().info(f'Object Graph Content:\n{json.dumps(object_graph, indent=2)}')
- else:
- self.get_logger().info('Object Graph: Empty')
- elif key == 'rtdl_syntax':
- rtdl_syntax = json.loads(request.params.values[i])
- self.get_logger().info('RTDL Syntax: Received')
- elif key == 'skill_primitive_specs':
- skill_primitive_specs = json.loads(request.params.values[i])
- self.get_logger().info('Skill/Primitive Specs: Received')
- except Exception as e:
- self.get_logger().warn(f'Failed to parse {key} from params: {e}')
-
- # Generate RTDL code
- self.get_logger().info('-' * 80)
- self.get_logger().info('Generating RTDL code...')
- rtdl_code = self._generate_rtdl(request.description, object_graph, rtdl_syntax, skill_primitive_specs)
-
- response.rtdl = rtdl_code
- response.rtdl_type = 'list' # Simple list-style RTDL
- response.stamp = self.get_clock().now().to_msg()
-
- self.get_logger().info('-' * 80)
- self.get_logger().info(f'Generated RTDL (type: {response.rtdl_type}, length: {len(rtdl_code)} chars)')
- self.get_logger().info(f'RTDL Code:\n{rtdl_code}')
- self.get_logger().info('=' * 80)
- return response
-
- def _generate_rtdl(self, description, object_graph=None, rtdl_syntax=None, skill_primitive_specs=None):
- """
- Generate simple list-style RTDL code from description using DeepSeek LLM.
- Format: JSON array of instructions, each with type, name, and params.
- """
- if not self.deepseek_client:
- raise RuntimeError('DeepSeek client not initialized')
-
- return self._generate_rtdl_with_deepseek(description, object_graph, rtdl_syntax, skill_primitive_specs)
-
- def _generate_rtdl_with_deepseek(self, description, object_graph=None, rtdl_syntax=None, skill_primitive_specs=None):
- """Generate RTDL using DeepSeek LLM API."""
- self.get_logger().info('Using DeepSeek LLM for task planning')
-
- # Build system prompt with RTDL syntax if provided
- if rtdl_syntax:
- system_prompt = f"""You are a robot task planning expert. Your task is to convert natural language task descriptions into RTDL (Real-Time Decision Logic) code.
-
-RTDL Syntax Specification:
-{json.dumps(rtdl_syntax, indent=2)}
-
-Important:
-- Only output valid JSON, no additional text
-- Follow the RTDL syntax specification exactly
-- ALWAYS include "object_id" field in each instruction (required to specify which object executes the instruction)
-- Use object_id from the object graph (usually the robot object)
-- Use skills/primitives available in the specified object's registered_skills/registered_primitives
-- Extract object IDs and locations from the description and object graph
-- Keep instructions simple and sequential"""
- else:
- # Default RTDL format
- system_prompt = """You are a robot task planning expert. Your task is to convert natural language task descriptions into RTDL (Real-Time Decision Logic) code.
-
-RTDL Format:
-- Output a JSON array of instructions
-- Each instruction is a JSON object with:
- - "object_id": string (REQUIRED) - ID of the object that executes this instruction (usually robot, e.g., "robot_001")
- - "type": "skill" or "primitive"
- - "name": skill/primitive name (e.g., "skl::pick", "skl::place", "skl::navigate", "prm::arm.move.ee") # remember to prefix with "skl::" or "prm::"
- - "params": JSON object with skill/primitive parameters
-
-Example:
-[
- {"object_id": "robot_001", "type": "skill", "name": "skl::pick", "params": {"target": "cup_001"}},
- {"object_id": "robot_001", "type": "skill", "name": "skl::place", "params": {"target": "cup_001", "destination": "table_001"}}
-]
-
-Important:
-- Only output valid JSON, no additional text
-- ALWAYS include "object_id" field in each instruction (required)
-- Use object_id from the object graph (usually the robot object)
-- Use skills/primitives available in the specified object's registered_skills/registered_primitives
-- Extract object IDs and locations from the description and object graph
-- Keep instructions simple and sequential"""
-
- user_prompt = f"Task description: {description}\n\n"
-
- # Add object graph information
- if object_graph:
- user_prompt += f"Available objects in environment:\n{json.dumps(object_graph, indent=2)}\n\n"
- # Extract robot object and its skills/primitives
- robot_objects = [obj for obj in object_graph if 'robot' in obj.get('label', '').lower() or 'robot' in obj.get('id', '').lower()]
- if robot_objects:
- robot = robot_objects[0]
- robot_id = robot.get('id', 'robot_001')
- robot_skills = robot.get('registered_skills', [])
- robot_primitives = robot.get('registered_primitives', [])
- user_prompt += f"Robot object ID: {robot_id}\n"
- user_prompt += f"Robot registered skills: {robot_skills}\n"
- user_prompt += f"Robot registered primitives: {robot_primitives}\n"
- user_prompt += f"IMPORTANT: Use object_id='{robot_id}' in all RTDL instructions to specify that this robot executes them.\n"
- user_prompt += "Use object IDs from the object graph when referencing objects in the task.\n"
- else:
- user_prompt += "WARNING: No robot object found in object graph. You must still include 'object_id' field in each instruction.\n"
- user_prompt += "Use object IDs from the object graph when referencing objects in the task.\n"
-
- # Add skill and primitive specifications
- if skill_primitive_specs:
- user_prompt += f"\nAvailable Skills and Primitives:\n{json.dumps(skill_primitive_specs, indent=2)}\n\n"
- user_prompt += "When generating RTDL, use the input/output parameter types from the specifications above.\n"
- user_prompt += "Ensure all parameters match the expected types (string, float, bool, geometry_msgs/msg/PoseStamped, etc.).\n"
-
- user_prompt += "\nGenerate RTDL code for this task:"
-
- # Log the full prompt
- self.get_logger().info('-' * 80)
- self.get_logger().info('DeepSeek LLM Prompt:')
- self.get_logger().info('System Prompt:')
- self.get_logger().info(system_prompt)
- self.get_logger().info('User Prompt:')
- self.get_logger().info(user_prompt)
- self.get_logger().info('-' * 80)
-
- # Call DeepSeek API
- import time
- start_time = time.time()
- self.get_logger().info('Calling DeepSeek API...')
- response = self.deepseek_client.chat.completions.create(
- model="deepseek-chat",
- messages=[
- {"role": "system", "content": system_prompt},
- {"role": "user", "content": user_prompt}
- ],
- temperature=0.1,
- )
- elapsed_time = time.time() - start_time
-
- llm_response = response.choices[0].message.content.strip()
- self.get_logger().info(f'DeepSeek API call completed in {elapsed_time:.2f} seconds')
- self.get_logger().info('-' * 80)
- self.get_logger().info('DeepSeek LLM Response (full):')
- self.get_logger().info(llm_response)
- self.get_logger().info('-' * 80)
-
- # Try to extract JSON from response (might have markdown code blocks)
- if llm_response.startswith('```'):
- # Remove markdown code blocks
- lines = llm_response.split('\n')
- json_start = None
- json_end = None
- for i, line in enumerate(lines):
- if line.strip().startswith('```'):
- if json_start is None:
- json_start = i + 1
- else:
- json_end = i
- break
- if json_start and json_end:
- llm_response = '\n'.join(lines[json_start:json_end])
-
- # Parse JSON response
- try:
- instructions = json.loads(llm_response)
- if not isinstance(instructions, list):
- instructions = [instructions]
-
- # Validate and format instructions
- formatted_instructions = []
- for inst in instructions:
- if isinstance(inst, dict):
- inst_object_id = inst.get('object_id', 'robot_001') # Default to robot_001 if missing
- inst_type = inst.get('type', 'skill')
- inst_name = inst.get('name', 'unknown')
- inst_params = inst.get('params', {})
- formatted_instructions.append({
- 'object_id': inst_object_id, # REQUIRED: object that executes this instruction
- 'type': inst_type,
- 'name': inst_name,
- 'params': inst_params
- })
-
- # Convert to JSON string (RTDL format)
- return json.dumps(formatted_instructions, indent=2)
- except json.JSONDecodeError as e:
- self.get_logger().error(f'Failed to parse DeepSeek response as JSON: {e}')
- self.get_logger().error(f'Response was: {llm_response}')
- raise RuntimeError(f'Failed to parse DeepSeek response as JSON: {e}')
-
- def _validate_api_key(self, api_key: str) -> bool:
- """
- Validate DeepSeek API key format.
- DeepSeek API keys typically start with 'sk-' and are at least 32 characters long.
- """
- if not api_key or not isinstance(api_key, str):
- return False
-
- # Remove whitespace
- api_key = api_key.strip()
-
- # Check minimum length (DeepSeek API keys are typically 32+ characters, including 'sk-' prefix)
- # Minimum: 'sk-' (3) + key part (32) = 35 characters
- if len(api_key) < 35:
- return False
-
- # Check if starts with 'sk-' (common format for API keys)
- if not api_key.startswith('sk-'):
- return False
-
- # Check if contains only valid characters (alphanumeric, hyphens, underscores)
- # After 'sk-' prefix
- key_part = api_key[3:]
- if not key_part.replace('-', '').replace('_', '').isalnum():
- return False
-
- # Check that key part has reasonable length (at least 32 characters)
- if len(key_part) < 32:
- return False
-
- return True
-
-
-def main(args=None):
- rclpy.init(args=args)
- task_plan_service = TaskPlanService()
- rclpy.spin(task_plan_service)
- task_plan_service.destroy_node()
- rclpy.shutdown()
-
-
-if __name__ == '__main__':
- main()
-
diff --git a/rust/provider/demo_package_service/rbnx/start_semantic_map.sh b/rust/provider/demo_package_service/rbnx/start_semantic_map.sh
deleted file mode 100755
index 384e737..0000000
--- a/rust/provider/demo_package_service/rbnx/start_semantic_map.sh
+++ /dev/null
@@ -1,137 +0,0 @@
-#!/bin/bash
-# SPDX-License-Identifier: MulanPSL-2.0
-# Start Semantic Map Service Script
-#
-# Start script for semantic_map service
-
-set -e
-
-SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-PACKAGE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
-cd "$PACKAGE_DIR"
-
-# Source ROS2 setup if available
-if [ -f /opt/ros/humble/setup.bash ]; then
- source /opt/ros/humble/setup.bash
-fi
-
-# Source robonix-sdk setup to make robonixpy SDK available
-# First try environment variable, then search upward from current directory
-ROBONIX_SDK_DIR=""
-if [ -n "$ROBONIX_SDK_PATH" ] && [ -f "$ROBONIX_SDK_PATH/install/setup.bash" ]; then
- ROBONIX_SDK_DIR="$ROBONIX_SDK_PATH"
-else
- # Search upward from package directory for robonix-sdk
- SEARCH_DIR="$PACKAGE_DIR"
- while [ "$SEARCH_DIR" != "/" ]; do
- if [ -d "$SEARCH_DIR/robonix-sdk" ] && [ -f "$SEARCH_DIR/robonix-sdk/install/setup.bash" ]; then
- ROBONIX_SDK_DIR="$SEARCH_DIR/robonix-sdk"
- break
- fi
- SEARCH_DIR="$(dirname "$SEARCH_DIR")"
- done
-fi
-
-# Fix library path issues with conda environment
-if [ -n "$CONDA_PREFIX" ]; then
- export LD_LIBRARY_PATH="/usr/lib/x86_64-linux-gnu:/lib/x86_64-linux-gnu:${LD_LIBRARY_PATH}"
- export LD_LIBRARY_PATH=$(echo "$LD_LIBRARY_PATH" | tr ':' '\n' | grep -v "$CONDA_PREFIX/lib" | tr '\n' ':' | sed 's/:$//')
-fi
-
-# Check if package needs to be built
-if [ -f "package.xml" ] && [ ! -d "install" ]; then
- echo "Package not built, building now..."
- if command -v colcon > /dev/null 2>&1; then
- colcon build --packages-select demo_service_provider 2>&1 | tail -20
- if [ $? -ne 0 ]; then
- echo "Build failed, continuing anyway..."
- fi
- else
- echo "Warning: colcon not found, cannot build package"
- fi
-fi
-
-# Setup Python path - add both source and install directories
-SYSTEM_PYTHON="/usr/bin/python3"
-if [ -f "$SYSTEM_PYTHON" ]; then
- # Add source directory to PYTHONPATH (for development)
- if [ -d "$PACKAGE_DIR/demo_service_provider" ]; then
- export PYTHONPATH="$PACKAGE_DIR:${PYTHONPATH}"
- fi
- # Add install directory to PYTHONPATH (for installed package)
- # Try multiple possible Python version paths
- for py_ver in python3.10 python3.11 python3.12 python3; do
- if [ -d "$PACKAGE_DIR/install/demo_service_provider/lib/$py_ver/site-packages" ]; then
- export PYTHONPATH="$PACKAGE_DIR/install/demo_service_provider/lib/$py_ver/site-packages:${PYTHONPATH}"
- break
- fi
- done
- PYTHON_CMD="$SYSTEM_PYTHON"
- LD_LIBRARY_PATH="/usr/lib/x86_64-linux-gnu:/lib/x86_64-linux-gnu:$LD_LIBRARY_PATH"
-else
- PYTHON_CMD="python3"
-fi
-
-# Source robonix-sdk setup AFTER setting PYTHONPATH to ensure it's preserved
-# First try environment variable, then search upward from current directory
-ROBONIX_SDK_DIR=""
-if [ -n "$ROBONIX_SDK_PATH" ] && [ -f "$ROBONIX_SDK_PATH/install/setup.bash" ]; then
- ROBONIX_SDK_DIR="$ROBONIX_SDK_PATH"
-else
- # Search upward from package directory for robonix-sdk
- SEARCH_DIR="$PACKAGE_DIR"
- while [ "$SEARCH_DIR" != "/" ]; do
- if [ -d "$SEARCH_DIR/robonix-sdk" ] && [ -f "$SEARCH_DIR/robonix-sdk/install/setup.bash" ]; then
- ROBONIX_SDK_DIR="$SEARCH_DIR/robonix-sdk"
- break
- fi
- SEARCH_DIR="$(dirname "$SEARCH_DIR")"
- done
-fi
-
-# Set COLCON_CURRENT_PREFIX to current package directory to fix setup script path issues
-export COLCON_CURRENT_PREFIX="$PACKAGE_DIR"
-
-# Source the local setup if available (but don't fail if it doesn't exist)
-if [ -f "install/setup.bash" ]; then
- # Suppress the error about build time path
- source install/setup.bash 2>/dev/null || true
-elif [ -f "install/setup.sh" ]; then
- source install/setup.sh 2>/dev/null || true
-fi
-
-# Source robonix-sdk setup AFTER local setup to ensure robonixpy is in PYTHONPATH
-if [ -n "$ROBONIX_SDK_DIR" ] && [ -f "$ROBONIX_SDK_DIR/install/setup.bash" ]; then
- # Source setup.bash which will add robonixpy to PYTHONPATH
- # Save current PYTHONPATH, source, then restore to ensure robonixpy is included
- OLD_PYTHONPATH="$PYTHONPATH"
- if source "$ROBONIX_SDK_DIR/install/setup.bash" 2>&1; then
- # Merge PYTHONPATH: robonix paths first, then old paths
- export PYTHONPATH="$PYTHONPATH:$OLD_PYTHONPATH"
- echo "[INFO] Sourced robonix-sdk setup.bash, PYTHONPATH includes robonixpy" >&2
- else
- echo "[WARN] Failed to source robonix-sdk setup.bash" >&2
- export PYTHONPATH="$OLD_PYTHONPATH"
- fi
-else
- echo "[WARN] robonix-sdk not found, robonixpy may not be available" >&2
-fi
-
-# Start semantic_map service
-# Try installed executable first, then ros2 run, then Python module
-if [ -f "$PACKAGE_DIR/install/demo_service_provider/bin/semantic_map_service" ]; then
- exec "$PACKAGE_DIR/install/demo_service_provider/bin/semantic_map_service"
-elif command -v ros2 > /dev/null 2>&1 && ros2 pkg list 2>/dev/null | grep -q "^demo_service_provider$"; then
- exec ros2 run demo_service_provider semantic_map_service
-else
- # Use Python module directly - ensure PYTHONPATH is set
- # Make sure source directory is in PYTHONPATH
- if [ -d "$PACKAGE_DIR/demo_service_provider" ]; then
- export PYTHONPATH="$PACKAGE_DIR:${PYTHONPATH}"
- fi
- # Debug: show PYTHONPATH
- echo "[DEBUG] PYTHONPATH=$PYTHONPATH" >&2
- echo "[DEBUG] Trying to run: $PYTHON_CMD -m demo_service_provider.semantic_map_service" >&2
- exec $PYTHON_CMD -m demo_service_provider.semantic_map_service
-fi
-
diff --git a/rust/provider/demo_package_service/rbnx/start_task_plan.sh b/rust/provider/demo_package_service/rbnx/start_task_plan.sh
deleted file mode 100755
index 73dfd5a..0000000
--- a/rust/provider/demo_package_service/rbnx/start_task_plan.sh
+++ /dev/null
@@ -1,137 +0,0 @@
-#!/bin/bash
-# SPDX-License-Identifier: MulanPSL-2.0
-# Start Task Plan Service Script
-#
-# Start script for task_plan service
-
-set -e
-
-SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-PACKAGE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
-cd "$PACKAGE_DIR"
-
-# Source ROS2 setup if available
-if [ -f /opt/ros/humble/setup.bash ]; then
- source /opt/ros/humble/setup.bash
-fi
-
-# Source robonix-sdk setup to make robonixpy SDK available
-# First try environment variable, then search upward from current directory
-ROBONIX_SDK_DIR=""
-if [ -n "$ROBONIX_SDK_PATH" ] && [ -f "$ROBONIX_SDK_PATH/install/setup.bash" ]; then
- ROBONIX_SDK_DIR="$ROBONIX_SDK_PATH"
-else
- # Search upward from package directory for robonix-sdk
- SEARCH_DIR="$PACKAGE_DIR"
- while [ "$SEARCH_DIR" != "/" ]; do
- if [ -d "$SEARCH_DIR/robonix-sdk" ] && [ -f "$SEARCH_DIR/robonix-sdk/install/setup.bash" ]; then
- ROBONIX_SDK_DIR="$SEARCH_DIR/robonix-sdk"
- break
- fi
- SEARCH_DIR="$(dirname "$SEARCH_DIR")"
- done
-fi
-
-# Fix library path issues with conda environment
-if [ -n "$CONDA_PREFIX" ]; then
- export LD_LIBRARY_PATH="/usr/lib/x86_64-linux-gnu:/lib/x86_64-linux-gnu:${LD_LIBRARY_PATH}"
- export LD_LIBRARY_PATH=$(echo "$LD_LIBRARY_PATH" | tr ':' '\n' | grep -v "$CONDA_PREFIX/lib" | tr '\n' ':' | sed 's/:$//')
-fi
-
-# Check if package needs to be built
-if [ -f "package.xml" ] && [ ! -d "install" ]; then
- echo "Package not built, building now..."
- if command -v colcon > /dev/null 2>&1; then
- colcon build --packages-select demo_service_provider 2>&1 | tail -20
- if [ $? -ne 0 ]; then
- echo "Build failed, continuing anyway..."
- fi
- else
- echo "Warning: colcon not found, cannot build package"
- fi
-fi
-
-# Setup Python path - add both source and install directories
-SYSTEM_PYTHON="/usr/bin/python3"
-if [ -f "$SYSTEM_PYTHON" ]; then
- # Add source directory to PYTHONPATH (for development)
- if [ -d "$PACKAGE_DIR/demo_service_provider" ]; then
- export PYTHONPATH="$PACKAGE_DIR:${PYTHONPATH}"
- fi
- # Add install directory to PYTHONPATH (for installed package)
- # Try multiple possible Python version paths
- for py_ver in python3.10 python3.11 python3.12 python3; do
- if [ -d "$PACKAGE_DIR/install/demo_service_provider/lib/$py_ver/site-packages" ]; then
- export PYTHONPATH="$PACKAGE_DIR/install/demo_service_provider/lib/$py_ver/site-packages:${PYTHONPATH}"
- break
- fi
- done
- PYTHON_CMD="$SYSTEM_PYTHON"
- LD_LIBRARY_PATH="/usr/lib/x86_64-linux-gnu:/lib/x86_64-linux-gnu:$LD_LIBRARY_PATH"
-else
- PYTHON_CMD="python3"
-fi
-
-# Source robonix-sdk setup AFTER setting PYTHONPATH to ensure it's preserved
-# First try environment variable, then search upward from current directory
-ROBONIX_SDK_DIR=""
-if [ -n "$ROBONIX_SDK_PATH" ] && [ -f "$ROBONIX_SDK_PATH/install/setup.bash" ]; then
- ROBONIX_SDK_DIR="$ROBONIX_SDK_PATH"
-else
- # Search upward from package directory for robonix-sdk
- SEARCH_DIR="$PACKAGE_DIR"
- while [ "$SEARCH_DIR" != "/" ]; do
- if [ -d "$SEARCH_DIR/robonix-sdk" ] && [ -f "$SEARCH_DIR/robonix-sdk/install/setup.bash" ]; then
- ROBONIX_SDK_DIR="$SEARCH_DIR/robonix-sdk"
- break
- fi
- SEARCH_DIR="$(dirname "$SEARCH_DIR")"
- done
-fi
-
-# Set COLCON_CURRENT_PREFIX to current package directory to fix setup script path issues
-export COLCON_CURRENT_PREFIX="$PACKAGE_DIR"
-
-# Source the local setup if available (but don't fail if it doesn't exist)
-if [ -f "install/setup.bash" ]; then
- # Suppress the error about build time path
- source install/setup.bash 2>/dev/null || true
-elif [ -f "install/setup.sh" ]; then
- source install/setup.sh 2>/dev/null || true
-fi
-
-# Source robonix-sdk setup AFTER local setup to ensure robonixpy is in PYTHONPATH
-if [ -n "$ROBONIX_SDK_DIR" ] && [ -f "$ROBONIX_SDK_DIR/install/setup.bash" ]; then
- # Source setup.bash which will add robonixpy to PYTHONPATH
- # Save current PYTHONPATH, source, then restore to ensure robonixpy is included
- OLD_PYTHONPATH="$PYTHONPATH"
- if source "$ROBONIX_SDK_DIR/install/setup.bash" 2>&1; then
- # Merge PYTHONPATH: robonix paths first, then old paths
- export PYTHONPATH="$PYTHONPATH:$OLD_PYTHONPATH"
- echo "[INFO] Sourced robonix-sdk setup.bash, PYTHONPATH includes robonixpy" >&2
- else
- echo "[WARN] Failed to source robonix-sdk setup.bash" >&2
- export PYTHONPATH="$OLD_PYTHONPATH"
- fi
-else
- echo "[WARN] robonix-sdk not found, robonixpy may not be available" >&2
-fi
-
-# Start task_plan service
-# Try installed executable first, then ros2 run, then Python module
-if [ -f "$PACKAGE_DIR/install/demo_service_provider/bin/task_plan_service" ]; then
- exec "$PACKAGE_DIR/install/demo_service_provider/bin/task_plan_service"
-elif command -v ros2 > /dev/null 2>&1 && ros2 pkg list 2>/dev/null | grep -q "^demo_service_provider$"; then
- exec ros2 run demo_service_provider task_plan_service
-else
- # Use Python module directly - ensure PYTHONPATH is set
- # Make sure source directory is in PYTHONPATH
- if [ -d "$PACKAGE_DIR/demo_service_provider" ]; then
- export PYTHONPATH="$PACKAGE_DIR:${PYTHONPATH}"
- fi
- # Debug: show PYTHONPATH
- echo "[DEBUG] PYTHONPATH=$PYTHONPATH" >&2
- echo "[DEBUG] Trying to run: $PYTHON_CMD -m demo_service_provider.task_plan_service" >&2
- exec $PYTHON_CMD -m demo_service_provider.task_plan_service
-fi
-
diff --git a/rust/provider/demo_service/.env.example b/rust/provider/demo_service/.env.example
new file mode 100644
index 0000000..7c9c6a3
--- /dev/null
+++ b/rust/provider/demo_service/.env.example
@@ -0,0 +1,19 @@
+# API Keys Configuration
+# Copy this file to .env and fill in your actual API key
+
+# DashScope (Qwen) API Key - used by both semantic_map and task_plan services
+# Get your API key from: https://dashscope.aliyun.com/
+# Base URL: https://dashscope.aliyuncs.com/compatible-mode/v1
+# Fill in one of the two (DASHSCOPE_API_KEY preferred; QWEN3_VL_API_KEY for backward compatibility)
+DASHSCOPE_API_KEY=sk-your-dashscope-api-key-here
+# QWEN3_VL_API_KEY=sk-your-dashscope-api-key-here
+
+# Semantic map: seconds between VLM API calls (default 15). Lower = more accurate but higher cost.
+# SEMANTIC_MAP_UPDATE_INTERVAL_SEC=15
+
+# Skip VLM when pose/image unchanged to save tokens (defaults below)
+# Pose: skip if position delta < 0.15m and yaw delta < 10°
+# SEMANTIC_MAP_POSE_POSITION_THRESHOLD_M=0.15
+# SEMANTIC_MAP_POSE_YAW_THRESHOLD_DEG=10.0
+# Image: skip if MSE of 32x32 grayscale < 80 (lower = stricter, need more change to trigger)
+# SEMANTIC_MAP_IMAGE_MSE_THRESHOLD=80.0
diff --git a/rust/provider/demo_service/.gitignore b/rust/provider/demo_service/.gitignore
new file mode 100644
index 0000000..53888e3
--- /dev/null
+++ b/rust/provider/demo_service/.gitignore
@@ -0,0 +1,28 @@
+# Python
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+.Python
+*.egg-info/
+dist/
+build/
+
+# Environment
+.env
+.env.local
+
+# Cache directory for debug images
+cache/
+*.jpg
+*.png
+
+# IDE
+.vscode/
+.idea/
+*.swp
+*.swo
+
+# ROS
+install/
+log/
diff --git a/rust/provider/demo_service/README.md b/rust/provider/demo_service/README.md
new file mode 100644
index 0000000..1a7a755
--- /dev/null
+++ b/rust/provider/demo_service/README.md
@@ -0,0 +1,161 @@
+# Demo Service Provider Package
+
+SPDX-License-Identifier: MulanPSL-2.0
+
+This package provides demo implementations of Robonix services:
+- **semantic_map**: Provides object-level representation of the environment using front camera and Qwen3-VL VLM
+- **task_plan**: Converts natural language task descriptions to RTDL code using Qwen LLM (DashScope)
+
+## Services
+
+### Semantic Map Service
+
+The semantic map service (`semantic_map_service`) uses the front camera primitive to capture RGB images and camera info, then uses Qwen3-VL VLM to detect objects and estimate their 3D coordinates in camera frame. It provides an object-level representation of the environment.
+
+**Service Interface**: `/demo_service/semantic_map/query`
+**Service Type**: `robonix_sdk/srv/service/semantic_map/QuerySemanticMap`
+
+**Features**:
+- Queries front camera primitive from OS using RobonixClient (from robonix_sdk.client)
+- Subscribes to RGB images and camera_info topics
+- Uses Qwen3-VL VLM to detect objects and estimate 3D positions in camera coordinate system
+- Returns objects with camera frame coordinates (world coordinates are empty for now)
+
+### Task Plan Service
+
+The task plan service (`task_plan_service`) converts natural language task descriptions into RTDL (Robot Task Description Langauge) code. It uses Qwen LLM API (DashScope, model `qwen-plus` by default) for intelligent task planning.
+
+**Service Interface**: `/demo_service/task_plan/plan`
+**Service Type**: `robonix_sdk/srv/service/task_plan/PlanTask`
+
+## Configuration
+
+### API Key (one key for both services)
+
+**semantic_map** and **task_plan** share the same DashScope API key; configure it once. The services read `DASHSCOPE_API_KEY` first; if unset, they fall back to `QWEN3_VL_API_KEY` (backward compatible).
+
+1. **Get an API key**
+ - Go to [DashScope Console](https://dashscope.aliyun.com/)
+ - Sign in, open API key management, create and copy a key
+
+2. **Configure**
+ - In the package root (same level as `setup.py`), copy `.env.example` to `.env`
+ - Set one of the following in `.env`:
+ ```
+ DASHSCOPE_API_KEY=sk-your-actual-api-key-here
+ ```
+ Or use the legacy variable (backward compatible):
+ ```
+ QWEN3_VL_API_KEY=sk-your-actual-api-key-here
+ ```
+
+3. **Optional**
+ - Task plan default model is `qwen-plus`; set `QWEN_LLM_MODEL` (e.g. `qwen-max`) to override
+ - **Semantic map API cost**: The semantic_map service calls DashScope Qwen3-VL in a background loop. Default update interval is **30 seconds** (was 5s). To change: set `SEMANTIC_MAP_UPDATE_INTERVAL_SEC` in `.env` (e.g. `60` for once per minute; lower values = more API calls and higher cost).
+ - Both services load `.env` on startup; check logs to confirm API initialization
+
+## Building and Installation
+
+### Prerequisites
+
+- ROS2 (Humble or later)
+- Python 3.8+
+- colcon build tools
+- **robonix-core** running (start from `rust` with `ROBONIX_WEB_ASSETS_DIR`, `ROBONIX_WEB_PORT`, and `eval $(make source-sdk)`; see [rust/README.md](../../README.md) Step 3)
+- DashScope (Qwen) API key (one key for both services; see `.env.example`)
+- Front camera primitive registered and running (for semantic map service)
+
+### Build
+
+```bash
+# From package directory
+colcon build --packages-select demo_service_provider
+```
+
+Or use robonix-cli:
+
+```bash
+rbnx deploy build
+```
+
+### Install Dependencies
+
+The package requires the following Python packages (automatically installed via setup.py):
+- `python-dotenv`: For loading environment variables from .env file
+- `openai`: For Qwen/DashScope API clients (OpenAI-compatible)
+- `robonix_sdk`: For querying primitives from Robonix OS (use `from robonix_sdk.client import RobonixClient`)
+- `cv-bridge`: For converting ROS images to OpenCV format
+- `numpy`: For numerical operations
+- `Pillow`: For image processing
+
+## Usage
+
+### Start Services
+
+After registering a recipe that includes this package (`rbnx deploy register `), start services via robonix-cli (pattern matches recipe item names):
+
+```bash
+# Start semantic map service (pattern matches srv::semantic_map)
+rbnx deploy start semantic_map
+
+# Start task plan service (pattern matches srv::task_plan)
+rbnx deploy start task_plan
+
+# Or start all items in the active recipe
+rbnx deploy start all
+```
+
+### Service Endpoints
+
+- **Semantic Map**: `/demo_service/semantic_map/query`
+- **Task Plan**: `/demo_service/task_plan/plan`
+
+## RTDL Format
+
+The task plan service generates RTDL code in JSON format:
+
+```json
+[
+ {
+ "type": "skill",
+ "name": "pick",
+ "params": {
+ "target": "cup_001"
+ }
+ },
+ {
+ "type": "skill",
+ "name": "place",
+ "params": {
+ "destination": "table_001"
+ }
+ }
+]
+```
+
+## Troubleshooting
+
+### API Not Working
+
+**Qwen3-VL API Issues:**
+1. Check that `.env` file exists and contains `QWEN3_VL_API_KEY`
+2. Verify the API key is correct and has sufficient credits
+3. Check service logs for API errors
+4. Ensure front camera primitive is registered and publishing images
+
+**Qwen / DashScope API Issues:**
+1. Check that `.env` file exists and contains `DASHSCOPE_API_KEY`
+2. Verify the API key is correct and has sufficient credits
+3. Check service logs for API errors
+
+### Service Not Starting
+
+1. Ensure ROS2 is properly sourced: `source /opt/ros/humble/setup.bash`
+2. Check that robonix-sdk is built and sourced
+3. Verify Python dependencies are installed
+4. Check service logs in `rbnx/` directory
+
+## License
+
+MulanPSL-2.0
+
diff --git a/rust/provider/demo_service/demo_service_provider/semantic_map_service.py b/rust/provider/demo_service/demo_service_provider/semantic_map_service.py
new file mode 100644
index 0000000..534d241
--- /dev/null
+++ b/rust/provider/demo_service/demo_service_provider/semantic_map_service.py
@@ -0,0 +1,1333 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: MulanPSL-2.0
+# Semantic Map Service
+#
+# Real semantic map service implementation using front camera and qwen3-vl VLM.
+# Queries camera primitives from OS, subscribes to RGB images and camera_info,
+# uses qwen3-vl to detect objects and estimate their 3D coordinates in camera frame.
+""""""
+
+import os
+import json
+import base64
+import math
+import threading
+import time
+import yaml
+import numpy as np
+from pathlib import Path
+from datetime import datetime
+import yaml
+import rclpy
+from rclpy.node import Node
+from rclpy.qos import (
+ QoSProfile,
+ ReliabilityPolicy,
+ HistoryPolicy,
+ DurabilityPolicy,
+)
+from robonix_sdk.srv import QuerySemanticMap
+from robonix_sdk.msg import (
+ Object,
+ FrameMapping,
+ Point3D,
+ BoundingBox,
+)
+from sensor_msgs.msg import Image, CameraInfo
+from geometry_msgs.msg import PoseStamped, PoseWithCovarianceStamped
+from cv_bridge import CvBridge
+from dotenv import load_dotenv
+from openai import OpenAI
+from robonix_sdk.client import RobonixClient
+
+
+class SemanticMapService(Node):
+ """Implements semantic_map service using front camera and qwen3-vl VLM."""
+
+ def __init__(self, config_filename=None):
+ super().__init__("demo_semantic_map_service")
+ self.config_filename = config_filename
+
+ current_file = Path(__file__).resolve()
+ package_root = current_file.parent
+ while package_root != package_root.parent:
+ if (package_root / "setup.py").exists():
+ break
+ package_root = package_root.parent
+ if not (package_root / "setup.py").exists():
+ package_root = current_file.parent.parent
+
+ load_dotenv(package_root / ".env")
+ # Same key as task_plan: DASHSCOPE_API_KEY (or legacy QWEN3_VL_API_KEY)
+ self.qwen_api_key = os.getenv("DASHSCOPE_API_KEY") or os.getenv("QWEN3_VL_API_KEY")
+ if not self.qwen_api_key:
+ self.get_logger().error(
+ "DASHSCOPE_API_KEY not found in .env file. "
+ "Please configure DASHSCOPE_API_KEY in .env file."
+ )
+ raise ValueError("DASHSCOPE_API_KEY not found in .env file.")
+
+ try:
+ self.qwen_client = OpenAI(
+ base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
+ api_key=self.qwen_api_key,
+ )
+ self.qwen_model = "qwen3-vl-plus"
+ self.get_logger().info(
+ f"Qwen3-VL API client initialized with model: {self.qwen_model}"
+ )
+ except Exception as e:
+ self.get_logger().error(f"Failed to initialize Qwen3-VL API client: {e}")
+ raise
+
+ # Initialize Robonix client helper
+ self.robonix = RobonixClient(self, self.get_logger())
+ self.query_primitive_client = self.robonix.create_query_primitive_client()
+
+ self.cv_bridge = CvBridge()
+ self.cache_dir = package_root / "cache"
+ try:
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
+ self.get_logger().info(f"Cache directory: {self.cache_dir}")
+ except Exception as e:
+ self.get_logger().error(
+ f"Failed to create cache directory {self.cache_dir}: {e}"
+ )
+ raise
+
+ self.rgb_image_topic = None
+ self.camera_info_topic = None
+ self.latest_rgb_image = None
+ self.latest_camera_info = None
+ self.image_counter = 0
+
+ self.pose_topic = None
+ self.latest_pose = None
+ self.pose_history = []
+ self.pose_history_lock = threading.Lock()
+
+ self.semantic_map_memory = {}
+ self.memory_lock = threading.Lock()
+
+ self.label_counters = {}
+ self.id_generation_lock = threading.Lock()
+
+ self.update_thread = None
+ self.update_thread_running = False
+ # VLM API (Aliyun DashScope) is charged per request; default 30s to reduce cost
+ default_interval = 15.0
+ try:
+ self.update_interval = float(
+ os.getenv("SEMANTIC_MAP_UPDATE_INTERVAL_SEC", str(default_interval))
+ )
+ if self.update_interval < 5.0:
+ self.update_interval = 5.0
+ except (TypeError, ValueError):
+ self.update_interval = default_interval
+ self.get_logger().info(
+ f"Semantic map VLM update interval: {self.update_interval}s "
+ "(set SEMANTIC_MAP_UPDATE_INTERVAL_SEC to change; lower = more API cost)"
+ )
+
+ # Skip VLM when pose/image unchanged to save tokens
+ self.last_vlm_pose = None # (x, y, z, yaw) when last VLM was called
+ self.last_vlm_image_lowres = None # 32x32 grayscale for similarity
+ self._last_vlm_lock = threading.Lock()
+
+ try:
+ self.pose_position_threshold = float(
+ os.getenv("SEMANTIC_MAP_POSE_POSITION_THRESHOLD_M", "0.15"))
+ except (TypeError, ValueError):
+ self.pose_position_threshold = 0.15
+ try:
+ yaw_deg = float(
+ os.getenv("SEMANTIC_MAP_POSE_YAW_THRESHOLD_DEG", "10.0"))
+ self.pose_yaw_threshold_rad = math.radians(yaw_deg)
+ except (TypeError, ValueError):
+ self.pose_yaw_threshold_rad = math.radians(10.0)
+ try:
+ self.image_mse_threshold = float(
+ os.getenv("SEMANTIC_MAP_IMAGE_MSE_THRESHOLD", "80.0"))
+ except (TypeError, ValueError):
+ self.image_mse_threshold = 80.0
+
+ self.get_logger().info(
+ f"VLM skip: pose_threshold={self.pose_position_threshold}m / "
+ f"{math.degrees(self.pose_yaw_threshold_rad):.1f}°, "
+ f"image_mse_threshold={self.image_mse_threshold}"
+ )
+
+ self._query_camera_primitives()
+ self._query_pose_primitive()
+
+ if self.rgb_image_topic is None:
+ raise ValueError("Failed to get rgb_image_topic from camera primitive")
+
+ self.rgb_subscriber = self.create_subscription(
+ Image, self.rgb_image_topic, self.rgb_image_callback, 10
+ )
+ self.get_logger().info(f"Subscribed to RGB image: {self.rgb_image_topic}")
+
+ if self.rgb_image_topic.endswith("/image_raw"):
+ self.camera_info_topic = self.rgb_image_topic.replace(
+ "/image_raw", "/camera_info"
+ )
+ else:
+ import re
+
+ match = re.match(r"(.+)/(rgb|depth)/image_raw", self.rgb_image_topic)
+ if match:
+ self.camera_info_topic = (
+ f"{match.group(1)}/{match.group(2)}/camera_info"
+ )
+ else:
+ self.get_logger().error(
+ f"Cannot infer camera_info topic from image topic: {self.rgb_image_topic}"
+ )
+ raise ValueError(
+ f"Cannot infer camera_info topic from image topic: {self.rgb_image_topic}"
+ )
+
+ self.camera_info_subscriber = self.create_subscription(
+ CameraInfo, self.camera_info_topic, self.camera_info_callback, 10
+ )
+ self.get_logger().info(f"Subscribed to camera info: {self.camera_info_topic}")
+
+ if self.pose_topic:
+ pose_qos = QoSProfile(
+ reliability=ReliabilityPolicy.RELIABLE,
+ history=HistoryPolicy.KEEP_LAST,
+ depth=10,
+ durability=DurabilityPolicy.VOLATILE,
+ )
+ self.pose_subscriber = self.create_subscription(
+ PoseWithCovarianceStamped,
+ self.pose_topic,
+ self.pose_cov_callback,
+ pose_qos,
+ )
+ self.get_logger().info(
+ f"Subscribed to robot pose (PoseWithCovarianceStamped from prm::base.pose.cov): {self.pose_topic} with RELIABLE QoS"
+ )
+
+ import time
+
+ wait_start = time.time()
+ wait_timeout = 2.0
+ while not self.latest_pose and (time.time() - wait_start) < wait_timeout:
+ rclpy.spin_once(self, timeout_sec=0.1)
+ if self.latest_pose:
+ self.get_logger().info(
+ f"Received initial pose: x={self.latest_pose.pose.position.x:.2f}, y={self.latest_pose.pose.position.y:.2f}"
+ )
+ else:
+ self.get_logger().info(
+ "No pose message received yet - will continue listening. Robot object will be created when pose becomes available."
+ )
+ else:
+ self.get_logger().warn(
+ "No pose topic available - robot object will use default position (0,0,0)"
+ )
+
+ self.service = self.create_service(
+ QuerySemanticMap, "/demo_service/semantic_map/query", self.query_callback
+ )
+
+ self.update_thread_running = True
+ self.update_thread = threading.Thread(
+ target=self._background_update_loop, daemon=True
+ )
+ self.update_thread.start()
+ self.get_logger().info("Started background semantic map update thread")
+
+ # Load manual objects from config file
+ self._load_manual_objects_from_config(package_root)
+
+ self.get_logger().info("Semantic map service started")
+ self.get_logger().info(" Service: /demo_service/semantic_map/query")
+ self.get_logger().info(
+ " Using Qwen3-VL for object detection (background updates)"
+ )
+
+ def _query_camera_primitives(self):
+ """Query front camera primitives from OS with retry logic. Exits if failed."""
+ self.rgb_image_topic = self.robonix.query_primitive_and_extract_field(
+ "prm::camera.rgb",
+ field_name="image",
+ filter_dict={"camera": "front"},
+ max_retries=5,
+ retry_delay=2.0,
+ wait_timeout=10.0,
+ call_timeout=3.0,
+ raise_on_error=True,
+ raise_on_missing_field=True,
+ log_success=True,
+ )
+
+ def _query_pose_primitive(self):
+ """Query robot pose primitive from OS with retry logic."""
+ self.pose_topic = self.robonix.query_primitive_and_extract_field(
+ "prm::base.pose.cov",
+ field_name="pose",
+ filter_dict=None,
+ max_retries=5,
+ retry_delay=2.0,
+ wait_timeout=10.0,
+ call_timeout=3.0,
+ raise_on_error=False, # Don't raise, just log warning
+ raise_on_missing_field=False, # Don't raise, just log warning
+ log_success=True,
+ )
+
+ def rgb_image_callback(self, msg):
+ """Callback for RGB image messages."""
+ self.latest_rgb_image = msg
+ try:
+ cv_image = self.cv_bridge.imgmsg_to_cv2(msg, "rgb8")
+ from PIL import Image as PILImage
+
+ pil_image = PILImage.fromarray(cv_image)
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
+ image_filename = (
+ self.cache_dir / f"rgb_{timestamp}_{self.image_counter:04d}.jpg"
+ )
+ pil_image.save(image_filename, quality=95)
+ self.image_counter += 1
+
+ if self.image_counter > 20:
+ image_files = sorted(self.cache_dir.glob("rgb_*.jpg"))
+ if len(image_files) > 20:
+ for old_file in image_files[:-20]:
+ old_file.unlink()
+ self.image_counter = 20
+
+ self.get_logger().debug(f"Saved RGB image to cache: {image_filename.name}")
+ except Exception as e:
+ self.get_logger().warn(f"Failed to save RGB image to cache: {e}")
+
+ def camera_info_callback(self, msg):
+ """Callback for camera info messages."""
+ self.latest_camera_info = msg
+
+ def pose_cov_callback(self, msg):
+ """Callback for PoseWithCovarianceStamped messages."""
+ pose_stamped = PoseStamped()
+ pose_stamped.header = msg.header
+ pose_stamped.pose = msg.pose.pose
+ was_none = self.latest_pose is None
+ self.latest_pose = pose_stamped
+
+ with self.pose_history_lock:
+ stamp_sec = (
+ float(msg.header.stamp.sec) + float(msg.header.stamp.nanosec) / 1e9
+ )
+ self.pose_history.append((stamp_sec, pose_stamped))
+ if len(self.pose_history) > 100:
+ self.pose_history.pop(0)
+
+ if was_none:
+ self.get_logger().info(
+ f"Received first pose update (PoseWithCovarianceStamped): x={msg.pose.pose.position.x:.2f}, y={msg.pose.pose.position.y:.2f}, z={msg.pose.pose.position.z:.2f} - robot object will now be available"
+ )
+ else:
+ self.get_logger().debug(
+ f"Received pose update (PoseWithCovarianceStamped): x={msg.pose.pose.position.x:.2f}, y={msg.pose.pose.position.y:.2f}, z={msg.pose.pose.position.z:.2f}"
+ )
+
+ def query_callback(self, request, response):
+ """Handle semantic map query request - returns latest memory state immediately."""
+ self.get_logger().info(f"Received query with types filter: {request.types}")
+
+ snapshot_pose = self.latest_pose
+ with self.memory_lock:
+ memory_copy = dict(self.semantic_map_memory)
+
+ memory_objects = self._get_all_objects_from_memory_unlocked(
+ memory_copy, request.types, snapshot_pose=snapshot_pose
+ )
+
+ seen_ids = set()
+ unique_objects = []
+ for obj in memory_objects:
+ if obj.id not in seen_ids:
+ unique_objects.append(obj)
+ seen_ids.add(obj.id)
+
+ response.objects = unique_objects
+ response.stamp = self.get_clock().now().to_msg()
+
+ self.get_logger().debug(
+ f"Returning {len(unique_objects)} objects from memory (immediate response)"
+ )
+ return response
+
+ def _background_update_loop(self):
+ """Background thread that continuously updates semantic map by calling VLM."""
+ self.get_logger().info("Background update loop started")
+
+ while self.update_thread_running:
+ try:
+ if not self.latest_rgb_image or not self.latest_camera_info:
+ self.get_logger().debug("Waiting for image/camera_info...")
+ time.sleep(1.0)
+ continue
+
+ snapshot_image = self.latest_rgb_image
+ snapshot_camera_info = self.latest_camera_info
+ snapshot_stamp = snapshot_image.header.stamp
+ image_stamp_sec = (
+ float(snapshot_stamp.sec) + float(snapshot_stamp.nanosec) / 1e9
+ )
+ snapshot_pose = self._find_pose_by_timestamp(image_stamp_sec)
+
+ # If no synchronized pose found, use latest pose if available
+ if not snapshot_pose:
+ if self.latest_pose:
+ snapshot_pose = self.latest_pose
+ self.get_logger().debug(
+ f"No synchronized pose found for image timestamp {image_stamp_sec:.3f}, "
+ f"using latest available pose (robot_pos=[{snapshot_pose.pose.position.x:.2f}, "
+ f"{snapshot_pose.pose.position.y:.2f}, {snapshot_pose.pose.position.z:.2f}])"
+ )
+ else:
+ self.get_logger().warn(
+ f"No pose available for image timestamp {image_stamp_sec:.3f}. "
+ f"Skipping this update."
+ )
+ time.sleep(self.update_interval)
+ continue
+ else:
+ # Log synchronized pose info
+ pose_stamp_sec = (
+ float(snapshot_pose.header.stamp.sec)
+ + float(snapshot_pose.header.stamp.nanosec) / 1e9
+ )
+ time_diff = abs(pose_stamp_sec - image_stamp_sec)
+ self.get_logger().info(
+ f"Using synchronized pose: image_t={image_stamp_sec:.3f}, pose_t={pose_stamp_sec:.3f}, "
+ f"diff={time_diff * 1000:.1f}ms, robot_pos=[{snapshot_pose.pose.position.x:.2f}, "
+ f"{snapshot_pose.pose.position.y:.2f}, {snapshot_pose.pose.position.z:.2f}]"
+ )
+
+ try:
+ cv_image = self.cv_bridge.imgmsg_to_cv2(snapshot_image, "rgb8")
+ except Exception as e:
+ self.get_logger().error(f"Failed to convert image: {e}")
+ time.sleep(self.update_interval)
+ continue
+
+ # Skip VLM if pose or image unchanged to save tokens
+ if self._pose_unchanged(snapshot_pose):
+ self.get_logger().info(
+ "Skipping VLM: robot pose unchanged (saving tokens)"
+ )
+ time.sleep(self.update_interval)
+ continue
+ if self._image_unchanged(cv_image):
+ self.get_logger().info(
+ "Skipping VLM: image similar to last (saving tokens)"
+ )
+ time.sleep(self.update_interval)
+ continue
+
+ from PIL import Image as PILImage
+ import io
+
+ pil_image = PILImage.fromarray(cv_image)
+ buffer = io.BytesIO()
+ pil_image.save(buffer, format="JPEG")
+ image_base64 = base64.b64encode(buffer.getvalue()).decode("utf-8")
+
+ camera_info_text = self._format_camera_info(snapshot_camera_info)
+ self.get_logger().info(
+ "Calling VLM for object detection with distance and direction..."
+ )
+ detected_objects = self._detect_objects_with_vlm(
+ image_base64, camera_info_text, snapshot_camera_info, []
+ )
+
+ with self.memory_lock:
+ processed_objects = self._process_detected_objects(
+ detected_objects, snapshot_pose=snapshot_pose
+ )
+ memory_count = len(self.semantic_map_memory)
+ self.get_logger().info(
+ f"Updated semantic map: {len(processed_objects)} new objects processed, {memory_count} total objects in memory"
+ )
+
+ # Remember pose and image so next cycle can skip VLM if unchanged
+ with self._last_vlm_lock:
+ self.last_vlm_pose = self._pose_to_tuple(snapshot_pose)
+ lowres = self._compute_image_lowres(cv_image)
+ if lowres is not None:
+ self.last_vlm_image_lowres = lowres.copy()
+
+ time.sleep(self.update_interval)
+
+ except Exception as e:
+ self.get_logger().error(f"Error in background update loop: {e}")
+ import traceback
+
+ self.get_logger().error(f"Traceback:\n{traceback.format_exc()}")
+ time.sleep(self.update_interval) # Wait before retrying
+
+ self.get_logger().info("Background update loop stopped")
+
+ def _pose_to_tuple(self, pose_stamped):
+ """Extract (x, y, z, yaw) from PoseStamped for comparison."""
+ if not pose_stamped or not pose_stamped.pose:
+ return None
+ p = pose_stamped.pose.position
+ o = pose_stamped.pose.orientation
+ yaw = math.atan2(
+ 2.0 * (o.w * o.z + o.x * o.y),
+ 1.0 - 2.0 * (o.y * o.y + o.z * o.z),
+ )
+ return (p.x, p.y, p.z, yaw)
+
+ def _pose_unchanged(self, current_pose_stamped):
+ """Return True if current pose is close to last VLM pose (skip VLM to save tokens)."""
+ with self._last_vlm_lock:
+ last = self.last_vlm_pose
+ if last is None:
+ return False
+ curr = self._pose_to_tuple(current_pose_stamped)
+ if curr is None:
+ return False
+ dx = curr[0] - last[0]
+ dy = curr[1] - last[1]
+ dz = curr[2] - last[2]
+ position_delta = math.sqrt(dx * dx + dy * dy + dz * dz)
+ yaw_delta = abs(curr[3] - last[3])
+ if yaw_delta > math.pi:
+ yaw_delta = 2.0 * math.pi - yaw_delta
+ return (
+ position_delta < self.pose_position_threshold
+ and yaw_delta < self.pose_yaw_threshold_rad
+ )
+
+ def _compute_image_lowres(self, cv_image, size=(32, 32)):
+ """Compute small grayscale image for similarity (no extra deps)."""
+ try:
+ if len(cv_image.shape) == 3:
+ gray = np.dot(cv_image[..., :3], [0.299, 0.587, 0.114]).astype(np.uint8)
+ else:
+ gray = cv_image
+ from PIL import Image as PILImage
+ pil = PILImage.fromarray(gray)
+ # 2 = BILINEAR (PIL.Image.Resampling.BILINEAR in Pillow 9+)
+ pil_small = pil.resize(size, 2)
+ return np.array(pil_small, dtype=np.uint8)
+ except Exception:
+ return None
+
+ def _image_unchanged(self, cv_image):
+ """Return True if image is very similar to last VLM image (skip VLM to save tokens)."""
+ with self._last_vlm_lock:
+ last_lowres = self.last_vlm_image_lowres
+ if last_lowres is None:
+ return False
+ curr_lowres = self._compute_image_lowres(cv_image)
+ if curr_lowres is None:
+ return False
+ mse = float(np.mean((curr_lowres.astype(np.float64) - last_lowres.astype(np.float64)) ** 2))
+ return mse < self.image_mse_threshold
+
+ def _format_camera_info(self, camera_info):
+ """Format camera info as text for VLM prompt."""
+ fx = camera_info.k[0] if len(camera_info.k) > 0 else 0
+ fy = camera_info.k[4] if len(camera_info.k) > 4 else 0
+ cx = camera_info.k[2] if len(camera_info.k) > 2 else 0
+ cy = camera_info.k[5] if len(camera_info.k) > 5 else 0
+ return f"Camera parameters: fx={fx:.2f}, fy={fy:.2f}, cx={cx:.2f}, cy={cy:.2f}, resolution={camera_info.width}x{camera_info.height}"
+
+ def _detect_objects_with_vlm(
+ self, image_base64, camera_info_text, camera_info, type_filter
+ ):
+ """Use Qwen3-VL to detect objects and estimate distance + direction vector."""
+ prompt = f"""Analyze this image and detect all visible objects. For each object, provide:
+1. Object label/name
+2. 2D bounding box coordinates (x_min, y_min, x_max, y_max) in pixels
+3. Estimated distance from camera in meters (straight-line 3D distance to object center)
+4. Estimated object size (width, height, depth in meters)
+
+Camera info: {camera_info_text}
+
+IMPORTANT: Only detect actual objects/items, NOT structural elements or surfaces.
+DO NOT include:
+- Walls, walls, wall surfaces
+- Floor, floor surfaces, ground
+- Ceiling, ceiling surfaces
+- Any architectural elements or room structures
+
+Only include discrete, movable, or identifiable objects such as:
+- Furniture (tables, chairs, sofas, cabinets, etc.)
+- Appliances (refrigerators, ovens, microwaves, etc.)
+- Electronics (TVs, computers, monitors, etc.)
+- Containers (boxes, bags, bottles, etc.)
+- People, animals
+- Other tangible items that can be interacted with
+
+CRITICAL for distance estimation:
+- The "distance" should be the actual 3D straight-line distance from the camera to the object center
+- Consider the object's position in the image: objects on the left/right sides are further away in 3D space
+- Use visual cues: object size, perspective, shadows, and relative positions to estimate accurate 3D distance
+- For objects at the image edges, the distance should account for the horizontal offset (objects on the left/right are further in 3D)
+
+Return the results as a JSON array, where each object has:
+{{
+ "label": "object_name",
+ "bbox_2d": [x_min, y_min, x_max, y_max],
+ "distance": ,
+ "size": [width, height, depth]
+}}
+
+Only include objects that are clearly visible. Estimate distance as accurately as possible based on visual cues, object size, and perspective."""
+
+ try:
+ # Call Qwen3-VL API
+ api_response = self.qwen_client.chat.completions.create(
+ model=self.qwen_model, # "qwen3-vl-plus"
+ messages=[
+ {
+ "role": "user",
+ "content": [
+ {
+ "type": "image_url",
+ "image_url": {
+ "url": f"data:image/jpeg;base64,{image_base64}"
+ },
+ },
+ {"type": "text", "text": prompt},
+ ],
+ }
+ ],
+ max_tokens=2000,
+ )
+
+ if not api_response.choices or len(api_response.choices) == 0:
+ self.get_logger().error("VLM API response has no choices")
+ return []
+
+ response_text = api_response.choices[0].message.content
+ if not response_text:
+ self.get_logger().error("VLM API response content is empty")
+ return []
+
+ self.get_logger().info(f"VLM response: {response_text[:200]}...")
+
+ import re
+
+ json_match = re.search(r"\[.*\]", response_text, re.DOTALL)
+ if json_match:
+ json_str = json_match.group()
+ if json_str:
+ objects_data = json.loads(json_str)
+ else:
+ self.get_logger().error("Failed to extract JSON from VLM response")
+ return []
+ else:
+ objects_data = json.loads(response_text)
+
+ objects = []
+ excluded_keywords = ["wall", "floor", "ceiling", "ground", "surface"]
+
+ for obj_data in objects_data:
+ label = obj_data.get("label", "").lower()
+ if any(keyword in label for keyword in excluded_keywords):
+ self.get_logger().debug(
+ f"Filtered out non-object: {obj_data.get('label', 'unknown')}"
+ )
+ continue
+
+ if type_filter and len(type_filter) > 0:
+ obj_type = self._infer_object_type(obj_data.get("label", ""))
+ if obj_type not in type_filter:
+ continue
+
+ obj = Object()
+ obj.label = obj_data.get("label", "unknown")
+ obj.id = self._generate_label_based_id(obj.label)
+ obj.registered_skills = []
+ obj.registered_primitives = []
+ obj.relations = []
+
+ bbox_2d = obj_data.get("bbox_2d", [0, 0, 640, 480])
+ distance = float(obj_data.get("distance", 1.0))
+ pos_cam = self._calculate_camera_coords_from_distance(
+ distance, bbox_2d, camera_info
+ )
+
+ frame_mapping = FrameMapping()
+ frame_mapping.frame_id = "head_front_camera_rgb_optical_frame"
+ frame_mapping.center = Point3D()
+ frame_mapping.center.x = pos_cam[0]
+ frame_mapping.center.y = pos_cam[1]
+ frame_mapping.center.z = pos_cam[2]
+
+ size = obj_data.get("size", [0.1, 0.1, 0.1])
+ bbox = BoundingBox()
+ bbox.scale_x = float(size[0]) if len(size) > 0 else 0.1
+ bbox.scale_y = float(size[1]) if len(size) > 1 else 0.1
+ bbox.scale_z = float(size[2]) if len(size) > 2 else 0.1
+ bbox.yaw = 0.0
+ frame_mapping.bbox = [bbox]
+
+ obj.frame_mapping = [frame_mapping]
+ objects.append(obj)
+
+ return objects
+
+ except Exception as e:
+ self.get_logger().error(f"Error calling Qwen3-VL API: {e}")
+ import traceback
+
+ self.get_logger().error(f"Traceback:\n{traceback.format_exc()}")
+ return []
+
+ def _generate_label_based_id(self, label):
+ """Generate a unique object ID based on label with index."""
+ label_lower = label.lower() if label else ""
+ normalized_label = self._normalize_label(label_lower)
+ sanitized_label = normalized_label.replace(" ", "_").replace("-", "_")
+ import re
+
+ sanitized_label = re.sub(r"[^a-z0-9_]", "_", sanitized_label)
+ sanitized_label = re.sub(r"_+", "_", sanitized_label)
+ sanitized_label = sanitized_label.strip("_")
+
+ with self.id_generation_lock:
+ existing_indices = set()
+ with self.memory_lock:
+ prefix = f"{sanitized_label}_"
+ for existing_id in self.semantic_map_memory.keys():
+ if existing_id.startswith(prefix):
+ suffix = existing_id[len(prefix) :]
+ if suffix.isdigit():
+ try:
+ existing_indices.add(int(suffix))
+ except ValueError:
+ pass
+
+ next_index = self.label_counters.get(sanitized_label, 0)
+ while next_index in existing_indices:
+ next_index += 1
+ self.label_counters[sanitized_label] = next_index + 1
+ return f"{sanitized_label}_{next_index}"
+
+ def _calculate_camera_coords_from_distance(self, distance, bbox_2d, camera_info):
+ """Calculate camera frame coordinates from distance and 2D bbox."""
+ x_min, y_min, x_max, y_max = bbox_2d
+ center_x = (x_min + x_max) / 2.0
+ center_y = (y_min + y_max) / 2.0
+
+ fx = camera_info.k[0] if len(camera_info.k) > 0 else 1.0
+ fy = camera_info.k[4] if len(camera_info.k) > 4 else 1.0
+ cx = camera_info.k[2] if len(camera_info.k) > 2 else camera_info.width / 2.0
+ cy = camera_info.k[5] if len(camera_info.k) > 5 else camera_info.height / 2.0
+
+ normalized_x = (center_x - cx) / fx
+ normalized_y = (center_y - cy) / fy
+ direction_magnitude = math.sqrt(normalized_x**2 + normalized_y**2 + 1.0)
+ scale_factor = distance / direction_magnitude
+
+ return [
+ normalized_x * scale_factor,
+ normalized_y * scale_factor,
+ 1.0 * scale_factor,
+ ]
+
+ def _transform_camera_to_map(self, camera_pos, robot_pose_stamped=None):
+ """Transform coordinates from camera frame to map frame."""
+ pose_to_use = (
+ robot_pose_stamped if robot_pose_stamped is not None else self.latest_pose
+ )
+ if not pose_to_use:
+ return None
+
+ robot_pose = pose_to_use.pose
+ robot_x = robot_pose.position.x
+ robot_y = robot_pose.position.y
+ robot_z = robot_pose.position.z
+
+ qx = robot_pose.orientation.x
+ qy = robot_pose.orientation.y
+ qz = robot_pose.orientation.z
+ qw = robot_pose.orientation.w
+ yaw = math.atan2(2.0 * (qw * qz + qx * qy), 1.0 - 2.0 * (qy * qy + qz * qz))
+
+ camera_offset_x = 0.3
+ camera_offset_y = 0.0
+ camera_offset_z = 0.2
+
+ base_x = camera_pos[2] + camera_offset_x
+ base_y = -camera_pos[0] + camera_offset_y
+ base_z = -camera_pos[1] + camera_offset_z
+
+ cos_yaw = math.cos(yaw)
+ sin_yaw = math.sin(yaw)
+
+ return [
+ robot_x + cos_yaw * base_x - sin_yaw * base_y,
+ robot_y + sin_yaw * base_x + cos_yaw * base_y,
+ robot_z + base_z,
+ ]
+
+ def _cluster_same_label_objects(self, objects_with_map_pos):
+ """Cluster objects with the same label that are close to each other."""
+ CLUSTER_DISTANCE_THRESHOLD = 1.5
+ if not objects_with_map_pos:
+ return []
+
+ objects_by_label = {}
+ for obj, map_pos in objects_with_map_pos:
+ normalized_label = self._normalize_label(obj.label)
+ if normalized_label not in objects_by_label:
+ objects_by_label[normalized_label] = []
+ objects_by_label[normalized_label].append((obj, map_pos))
+
+ clustered_objects = []
+ for label, obj_list in objects_by_label.items():
+ if len(obj_list) == 1:
+ clustered_objects.append(obj_list[0])
+ continue
+
+ clusters = []
+ for obj, map_pos in obj_list:
+ assigned = False
+ for cluster in clusters:
+ cluster_center = cluster["center"]
+ distance = math.sqrt(
+ (map_pos[0] - cluster_center[0]) ** 2
+ + (map_pos[1] - cluster_center[1]) ** 2
+ + (map_pos[2] - cluster_center[2]) ** 2
+ )
+ if distance < CLUSTER_DISTANCE_THRESHOLD:
+ cluster["objects"].append((obj, map_pos))
+ n = len(cluster["objects"])
+ cluster["center"] = [
+ sum(p[1][0] for p in cluster["objects"]) / n,
+ sum(p[1][1] for p in cluster["objects"]) / n,
+ sum(p[1][2] for p in cluster["objects"]) / n,
+ ]
+ assigned = True
+ break
+
+ if not assigned:
+ clusters.append({"objects": [(obj, map_pos)], "center": map_pos})
+
+ for cluster in clusters:
+ if len(cluster["objects"]) == 1:
+ clustered_objects.append(cluster["objects"][0])
+ else:
+ obj, _ = cluster["objects"][0]
+ avg_pos = cluster["center"]
+ for fm in obj.frame_mapping:
+ if fm.frame_id == "map":
+ fm.center.x = avg_pos[0]
+ fm.center.y = avg_pos[1]
+ fm.center.z = avg_pos[2]
+ break
+ self.get_logger().info(
+ f'Clustered {len(cluster["objects"])} objects with label "{obj.label}" at average position [{avg_pos[0]:.2f}, {avg_pos[1]:.2f}, {avg_pos[2]:.2f}]'
+ )
+ clustered_objects.append((obj, avg_pos))
+
+ return clustered_objects
+
+ def _process_detected_objects(self, detected_objects, snapshot_pose=None):
+ """Process detected objects: convert to map frame, cluster same-label objects, and merge with memory."""
+ if not snapshot_pose:
+ self.get_logger().error(
+ "snapshot_pose is None - cannot process objects without synchronized pose. "
+ "Skipping object processing to ensure accuracy."
+ )
+ return []
+
+ objects_with_map_pos = []
+ objects_without_map = []
+
+ for obj in detected_objects:
+ if not obj.frame_mapping or len(obj.frame_mapping) == 0:
+ continue
+
+ camera_frame = obj.frame_mapping[0]
+ camera_pos = [
+ camera_frame.center.x,
+ camera_frame.center.y,
+ camera_frame.center.z,
+ ]
+
+ map_pos = self._transform_camera_to_map(
+ camera_pos, robot_pose_stamped=snapshot_pose
+ )
+ if map_pos:
+ map_frame = FrameMapping()
+ map_frame.frame_id = "map"
+ map_frame.center = Point3D()
+ map_frame.center.x = map_pos[0]
+ map_frame.center.y = map_pos[1]
+ map_frame.center.z = map_pos[2]
+ map_frame.bbox = camera_frame.bbox.copy() if camera_frame.bbox else []
+ map_frame.texture = (
+ camera_frame.texture.copy() if camera_frame.texture else []
+ )
+ obj.frame_mapping.append(map_frame)
+ objects_with_map_pos.append((obj, map_pos))
+ else:
+ self.get_logger().warn(
+ f"Transform failed for object {obj.label} (id={obj.id}), skipping"
+ )
+ objects_without_map.append(obj)
+
+ clustered_objects = self._cluster_same_label_objects(objects_with_map_pos)
+ processed_objects = []
+ for obj, map_pos in clustered_objects:
+ merged = self._merge_with_memory(obj, map_pos)
+ if not merged:
+ self._add_to_memory(obj, map_pos)
+ processed_objects.append(obj)
+
+ processed_objects.extend(objects_without_map)
+ return processed_objects
+
+ def _normalize_label(self, label):
+ """Normalize label to handle variations (e.g., 'computer monitor' -> 'monitor')."""
+ label_lower = label.lower()
+ if "monitor" in label_lower or "computer monitor" in label_lower:
+ return "monitor"
+ if "office chair" in label_lower or "chair" in label_lower:
+ return "chair"
+ if "desk" in label_lower or "table" in label_lower:
+ return "desk"
+ if "cabinet" in label_lower or "shelf" in label_lower:
+ return "cabinet"
+ return label_lower
+
+ def _merge_with_memory(self, obj, map_pos):
+ """Try to merge object with existing objects in memory based on label and position."""
+ MERGE_DISTANCE_THRESHOLD = 1.5
+ obj_label_normalized = self._normalize_label(obj.label)
+ best_match = None
+ best_distance = float("inf")
+
+ for existing_id, existing_obj in self.semantic_map_memory.items():
+ existing_label_normalized = self._normalize_label(existing_obj.label)
+ if existing_label_normalized != obj_label_normalized:
+ continue
+
+ existing_map_pos = None
+ for fm in existing_obj.frame_mapping:
+ if fm.frame_id == "map":
+ existing_map_pos = [fm.center.x, fm.center.y, fm.center.z]
+ break
+
+ if not existing_map_pos:
+ continue
+
+ distance = math.sqrt(
+ (map_pos[0] - existing_map_pos[0]) ** 2
+ + (map_pos[1] - existing_map_pos[1]) ** 2
+ + (map_pos[2] - existing_map_pos[2]) ** 2
+ )
+
+ if distance < MERGE_DISTANCE_THRESHOLD and distance < best_distance:
+ best_match = existing_obj
+ best_distance = distance
+
+ if best_match:
+ weight_new = 0.3
+ weight_existing = 0.7
+ existing_map_pos = None
+ for fm in best_match.frame_mapping:
+ if fm.frame_id == "map":
+ existing_map_pos = [fm.center.x, fm.center.y, fm.center.z]
+ break
+
+ if existing_map_pos:
+ merged_x = (
+ weight_existing * existing_map_pos[0] + weight_new * map_pos[0]
+ )
+ merged_y = (
+ weight_existing * existing_map_pos[1] + weight_new * map_pos[1]
+ )
+ merged_z = (
+ weight_existing * existing_map_pos[2] + weight_new * map_pos[2]
+ )
+
+ for fm in best_match.frame_mapping:
+ if fm.frame_id == "map":
+ fm.center.x = merged_x
+ fm.center.y = merged_y
+ fm.center.z = merged_z
+ break
+
+ obj.id = best_match.id
+ self.get_logger().info(
+ f'Merged object "{obj.label}" with existing object (id={best_match.id}) at distance {best_distance:.2f}m'
+ )
+ return True
+
+ return False
+
+ def _add_to_memory(self, obj, map_pos):
+ """Add object to memory. Objects are never deleted once they have map coordinates."""
+ self.semantic_map_memory[obj.id] = obj
+ self.get_logger().info(
+ f'Added object "{obj.label}" (id={obj.id}) to memory at map position [{map_pos[0]:.2f}, {map_pos[1]:.2f}, {map_pos[2]:.2f}]'
+ )
+
+ def _get_all_objects_from_memory_unlocked(
+ self, memory_dict, type_filter, snapshot_pose=None
+ ):
+ """Get all objects from memory dict (unlocked version for query_callback)."""
+ objects_with_map_pos = []
+ seen_ids = set()
+
+ for obj_id, obj in memory_dict.items():
+ if obj_id in seen_ids:
+ continue
+
+ map_pos = None
+ for fm in obj.frame_mapping:
+ if fm.frame_id == "map":
+ map_pos = [fm.center.x, fm.center.y, fm.center.z]
+ break
+
+ if not map_pos:
+ continue
+
+ if type_filter and len(type_filter) > 0:
+ obj_type = self._infer_object_type(obj.label)
+ if obj_type not in type_filter:
+ continue
+
+ objects_with_map_pos.append((obj, map_pos))
+ seen_ids.add(obj_id)
+
+ clustered_objects = self._cluster_same_label_objects(objects_with_map_pos)
+ objects = []
+ for item in clustered_objects:
+ obj, _ = item if isinstance(item, tuple) else (item, None)
+ objects.append(obj)
+
+ robot_object = self._create_robot_object(snapshot_pose=snapshot_pose)
+ if robot_object and robot_object.id not in seen_ids:
+ if not type_filter or len(type_filter) == 0 or "robot" in type_filter:
+ objects.append(robot_object)
+ seen_ids.add(robot_object.id)
+
+ return objects
+
+ def _create_robot_object(self, snapshot_pose=None):
+ """Create robot object for semantic map. Only creates if pose is available."""
+ pose_to_use = snapshot_pose if snapshot_pose is not None else self.latest_pose
+ if not pose_to_use:
+ return None
+
+ obj = Object()
+ obj.id = "robot_self"
+ obj.label = "robot"
+ obj.registered_skills = []
+ obj.registered_primitives = []
+ obj.relations = []
+
+ map_frame = FrameMapping()
+ map_frame.frame_id = "map"
+ map_frame.center = Point3D()
+
+ robot_pose = pose_to_use.pose
+ map_frame.center.x = robot_pose.position.x
+ map_frame.center.y = robot_pose.position.y
+ map_frame.center.z = robot_pose.position.z
+
+ qx = robot_pose.orientation.x
+ qy = robot_pose.orientation.y
+ qz = robot_pose.orientation.z
+ qw = robot_pose.orientation.w
+ siny_cosp = 2.0 * (qw * qz + qx * qy)
+ cosy_cosp = 1.0 - 2.0 * (qy * qy + qz * qz)
+ yaw = math.atan2(siny_cosp, cosy_cosp)
+
+ self.get_logger().info(
+ f"Created robot object with pose: x={map_frame.center.x:.2f}, y={map_frame.center.y:.2f}, z={map_frame.center.z:.2f}, yaw={math.degrees(yaw):.1f}°"
+ )
+
+ bbox = BoundingBox()
+ bbox.scale_x = 0.5
+ bbox.scale_y = 0.5
+ bbox.scale_z = 1.0
+ bbox.yaw = yaw
+ map_frame.bbox = [bbox]
+ map_frame.texture = []
+
+ obj.frame_mapping = [map_frame]
+ return obj
+
+ def _find_pose_by_timestamp(self, target_stamp_sec):
+ """Find pose closest to target timestamp from pose history."""
+ with self.pose_history_lock:
+ if not self.pose_history:
+ return None
+
+ best_pose = None
+ best_diff = float("inf")
+ TOLERANCE_SEC = 0.5
+
+ for stamp_sec, pose in self.pose_history:
+ diff = abs(stamp_sec - target_stamp_sec)
+ if diff < best_diff and diff <= TOLERANCE_SEC:
+ best_diff = diff
+ best_pose = pose
+
+ if best_pose:
+ self.get_logger().debug(
+ f"Found matching pose: target_t={target_stamp_sec:.3f}, "
+ f"pose_t={best_diff + target_stamp_sec:.3f}, diff={best_diff * 1000:.1f}ms"
+ )
+ else:
+ self.get_logger().debug(
+ f"No pose within tolerance: target_t={target_stamp_sec:.3f}, "
+ f"history_size={len(self.pose_history)}, tolerance={TOLERANCE_SEC * 1000:.0f}ms"
+ )
+
+ return best_pose
+
+ def _infer_object_type(self, label):
+ """Infer object type from label (simple heuristic)."""
+ label_lower = label.lower()
+ if "table" in label_lower or "desk" in label_lower:
+ return "table"
+ elif "box" in label_lower or "container" in label_lower:
+ return "box"
+ elif "chair" in label_lower:
+ return "chair"
+ elif "robot" in label_lower:
+ return "robot"
+ elif "waypoint" in label_lower:
+ return "waypoint"
+ else:
+ return "object"
+
+ def _load_manual_objects_from_config(self, package_root):
+ """Load manual objects from config YAML file (e.g. building_map_config.yaml or webots_map_config.yaml)."""
+ # Resolve config filename: short name -> *_map_config.yaml, or use as-is if contains '.yaml'
+ if not self.config_filename:
+ config_name = "building_map_config.yaml"
+ elif self.config_filename.endswith(".yaml") or self.config_filename.endswith(".yml"):
+ config_name = self.config_filename
+ else:
+ config_name = f"{self.config_filename}_map_config.yaml"
+ config_path = package_root / "rbnx" / config_name
+
+ if not config_path.exists():
+ self.get_logger().info(
+ f"Config file not found at {config_path}, skipping manual objects loading"
+ )
+ return
+ self.get_logger().info(f"Loading manual objects from config: {config_name}")
+
+ try:
+ with open(config_path, "r", encoding="utf-8") as f:
+ config_data = yaml.safe_load(f)
+
+ if not config_data or "manual_objects" not in config_data:
+ self.get_logger().info("No manual_objects found in config file")
+ return
+
+ manual_objects_config = config_data.get("manual_objects", [])
+ if not manual_objects_config:
+ self.get_logger().info("manual_objects list is empty")
+ return
+
+ loaded_count = 0
+ for obj_config in manual_objects_config:
+ try:
+ obj = self._create_object_from_config(obj_config)
+ if obj:
+ # Generate ID if not provided, using same logic as auto-generated objects
+ if not obj.id or obj.id == "":
+ obj.id = self._generate_label_based_id(obj.label)
+
+ # Check if object already exists (by ID)
+ with self.memory_lock:
+ if obj.id in self.semantic_map_memory:
+ self.get_logger().warn(
+ f"Manual object with id '{obj.id}' already exists, skipping"
+ )
+ continue
+
+ # Add to memory
+ self.semantic_map_memory[obj.id] = obj
+ loaded_count += 1
+
+ # Log object position
+ map_pos = None
+ for fm in obj.frame_mapping:
+ if fm.frame_id == "map":
+ map_pos = [fm.center.x, fm.center.y, fm.center.z]
+ break
+
+ if map_pos:
+ self.get_logger().info(
+ f'Loaded manual object "{obj.label}" (id={obj.id}) at map position '
+ f'[{map_pos[0]:.2f}, {map_pos[1]:.2f}, {map_pos[2]:.2f}]'
+ )
+ else:
+ self.get_logger().info(
+ f'Loaded manual object "{obj.label}" (id={obj.id})'
+ )
+
+ except Exception as e:
+ self.get_logger().error(
+ f"Failed to load manual object from config: {e}"
+ )
+ import traceback
+ self.get_logger().error(f"Traceback:\n{traceback.format_exc()}")
+ continue
+
+ self.get_logger().info(
+ f"Successfully loaded {loaded_count} manual object(s) from config"
+ )
+
+ except Exception as e:
+ self.get_logger().error(
+ f"Failed to load manual objects from config file {config_path}: {e}"
+ )
+ import traceback
+ self.get_logger().error(f"Traceback:\n{traceback.format_exc()}")
+
+ def _create_object_from_config(self, obj_config):
+ """Create an Object from config dictionary."""
+ if "label" not in obj_config:
+ self.get_logger().error("Manual object config missing 'label' field")
+ return None
+
+ obj = Object()
+ obj.label = obj_config["label"]
+
+ # Use provided ID or None (will be auto-generated later)
+ obj.id = obj_config.get("id") or ""
+
+ # Set default empty lists for optional fields
+ obj.registered_skills = obj_config.get("registered_skills", [])
+ obj.registered_primitives = obj_config.get("registered_primitives", [])
+ obj.relations = obj_config.get("relations", [])
+
+ # Parse frame_mapping
+ if "frame_mapping" not in obj_config:
+ self.get_logger().error(
+ f"Manual object '{obj.label}' config missing 'frame_mapping' field"
+ )
+ return None
+
+ obj.frame_mapping = []
+ for fm_config in obj_config["frame_mapping"]:
+ frame_mapping = FrameMapping()
+ frame_mapping.frame_id = fm_config.get("frame_id", "map")
+
+ # Parse center
+ if "center" not in fm_config:
+ self.get_logger().error(
+ f"Manual object '{obj.label}' frame_mapping missing 'center' field"
+ )
+ continue
+
+ center_config = fm_config["center"]
+ frame_mapping.center = Point3D()
+ frame_mapping.center.x = float(center_config.get("x", 0.0))
+ frame_mapping.center.y = float(center_config.get("y", 0.0))
+ frame_mapping.center.z = float(center_config.get("z", 0.0))
+
+ # Parse bbox (optional, defaults to small box)
+ frame_mapping.bbox = []
+ if "bbox" in fm_config and fm_config["bbox"]:
+ for bbox_config in fm_config["bbox"]:
+ bbox = BoundingBox()
+ bbox.scale_x = float(bbox_config.get("scale_x", 0.1))
+ bbox.scale_y = float(bbox_config.get("scale_y", 0.1))
+ bbox.scale_z = float(bbox_config.get("scale_z", 0.1))
+ bbox.yaw = float(bbox_config.get("yaw", 0.0))
+ frame_mapping.bbox.append(bbox)
+ else:
+ # Default bbox for waypoints
+ bbox = BoundingBox()
+ bbox.scale_x = 0.1
+ bbox.scale_y = 0.1
+ bbox.scale_z = 0.1
+ bbox.yaw = 0.0
+ frame_mapping.bbox.append(bbox)
+
+ # Parse texture (optional, defaults to empty)
+ frame_mapping.texture = fm_config.get("texture", [])
+
+ obj.frame_mapping.append(frame_mapping)
+
+ if not obj.frame_mapping:
+ self.get_logger().error(
+ f"Manual object '{obj.label}' has no valid frame_mapping"
+ )
+ return None
+
+ return obj
+
+
+def main(args=None):
+ import argparse
+ import sys
+ parser = argparse.ArgumentParser(description="Semantic map service (manual objects from config)")
+ parser.add_argument(
+ "--config",
+ type=str,
+ default=None,
+ metavar="NAME",
+ help="Config name or file: 'building' (default), 'webots', or filename e.g. my_map_config.yaml",
+ )
+ # Parse only known args so rclpy can handle the rest
+ argv = args if args is not None else sys.argv[1:]
+ parsed, remaining = parser.parse_known_args(argv)
+ config_filename = parsed.config
+
+ rclpy.init(args=remaining)
+ semantic_map_service = None
+ try:
+ semantic_map_service = SemanticMapService(config_filename=config_filename)
+ rclpy.spin(semantic_map_service)
+ semantic_map_service.destroy_node()
+ except (RuntimeError, ValueError) as e:
+ import sys
+
+ print(f"FATAL: Failed to initialize semantic map service: {e}", file=sys.stderr)
+ if semantic_map_service is not None:
+ try:
+ semantic_map_service.destroy_node()
+ except Exception:
+ pass
+ try:
+ rclpy.shutdown()
+ except Exception:
+ pass
+ sys.exit(1)
+ except Exception as e:
+ import sys
+
+ print(f"FATAL: Unexpected error: {e}", file=sys.stderr)
+ if semantic_map_service is not None:
+ try:
+ semantic_map_service.destroy_node()
+ except Exception:
+ pass
+ try:
+ rclpy.shutdown()
+ except Exception:
+ pass
+ sys.exit(1)
+ finally:
+ try:
+ rclpy.shutdown()
+ except Exception:
+ pass
+
+
+if __name__ == "__main__":
+ main()
diff --git a/rust/provider/demo_service/demo_service_provider/task_plan_service.py b/rust/provider/demo_service/demo_service_provider/task_plan_service.py
new file mode 100644
index 0000000..5af0674
--- /dev/null
+++ b/rust/provider/demo_service/demo_service_provider/task_plan_service.py
@@ -0,0 +1,510 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: MulanPSL-2.0
+# Task Plan Service
+#
+# Demo task plan service implementation.
+# Converts natural language task description to RTDL code.
+# Uses Qwen LLM API (DashScope) for intelligent task planning.
+# Uses simple list-style RTDL format (assembly-like instruction list).
+
+import os
+import json
+from pathlib import Path
+import rclpy
+from rclpy.node import Node
+from robonix_sdk.srv import PlanTask
+from dotenv import load_dotenv
+from openai import OpenAI
+
+
+class TaskPlanService(Node):
+ """Implements task_plan service with Qwen LLM (DashScope) for RTDL generation."""
+
+ def __init__(self):
+ super().__init__("demo_task_plan_service")
+
+ # Load environment variables from .env file in package root directory
+ # Find package root directory (containing setup.py) by walking up from current file
+ current_file = Path(__file__).resolve()
+ package_root = current_file.parent
+ # Walk up directories to find package root (containing setup.py)
+ while package_root != package_root.parent:
+ if (package_root / "setup.py").exists():
+ break
+ package_root = package_root.parent
+ # If setup.py not found, use current file's parent's parent as fallback
+ if not (package_root / "setup.py").exists():
+ package_root = current_file.parent.parent
+
+ env_path = package_root / ".env"
+ load_dotenv(env_path)
+
+ # Same key as semantic_map: DASHSCOPE_API_KEY (or legacy QWEN3_VL_API_KEY)
+ self.api_key = os.getenv("DASHSCOPE_API_KEY") or os.getenv("QWEN3_VL_API_KEY")
+ if not self.api_key:
+ self.get_logger().error(
+ "DASHSCOPE_API_KEY (or QWEN3_VL_API_KEY) not found in .env file. "
+ "Please configure API key in .env file. See README.md for configuration."
+ )
+ raise ValueError(
+ "DASHSCOPE_API_KEY not found. "
+ "Please configure DASHSCOPE_API_KEY (or QWEN3_VL_API_KEY) in .env file."
+ )
+
+ # Validate API key format
+ if not self._validate_api_key(self.api_key):
+ self.get_logger().error(
+ "Invalid DASHSCOPE_API_KEY format. "
+ 'API key should start with "sk-" and be at least 35 characters long. '
+ "Please check your .env file."
+ )
+ raise ValueError(
+ "Invalid DASHSCOPE_API_KEY format. "
+ 'API key should start with "sk-" and be at least 35 characters long.'
+ )
+
+ # Qwen best LLM model for task planning (text); override via QWEN_LLM_MODEL env
+ self.qwen_model = os.getenv("QWEN_LLM_MODEL", "qwen-plus")
+
+ # Initialize Qwen client (DashScope OpenAI-compatible API)
+ try:
+ self.qwen_client = OpenAI(
+ base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
+ api_key=self.api_key,
+ )
+ self.get_logger().info(
+ f"Qwen API client initialized with model: {self.qwen_model}"
+ )
+ except Exception as e:
+ self.get_logger().error(f"Failed to initialize Qwen API client: {e}")
+ raise
+
+ # Create service
+ self.service = self.create_service(
+ PlanTask, "/demo_service/task_plan/plan", self.plan_callback
+ )
+
+ self.get_logger().info("Task plan service started")
+ self.get_logger().info(" Service: /demo_service/task_plan/plan")
+
+ def plan_callback(self, request, response):
+ """Handle task planning request."""
+ self.get_logger().info("=" * 80)
+ self.get_logger().info("Task Plan Service: Received planning request")
+ self.get_logger().info("=" * 80)
+ self.get_logger().info(f"Task Description: {request.description}")
+ self.get_logger().info(
+ f"Params keys: {request.params.keys if request.params else []}"
+ )
+
+ # Parse params to extract object graph, RTDL syntax, and skill/primitive specs
+ object_graph = None
+ rtdl_syntax = None
+ skill_primitive_specs = None
+ skill_specs = None
+
+ if request.params:
+ # Extract all params from Dict (keys and values arrays)
+ for i, key in enumerate(request.params.keys):
+ if i < len(request.params.values):
+ try:
+ if key == "object_graph":
+ object_graph = json.loads(request.params.values[i])
+ obj_count = (
+ len(object_graph)
+ if isinstance(object_graph, list)
+ else "unknown"
+ )
+ self.get_logger().info(
+ f"Object Graph: Found {obj_count} objects"
+ )
+ if object_graph:
+ self.get_logger().info(
+ f"Object Graph Content:\n{json.dumps(object_graph, indent=2)}"
+ )
+ else:
+ self.get_logger().info("Object Graph: Empty")
+ elif key == "rtdl_syntax":
+ rtdl_syntax = json.loads(request.params.values[i])
+ self.get_logger().info("RTDL Syntax: Received")
+ elif key == "skill_primitive_specs":
+ skill_primitive_specs = json.loads(request.params.values[i])
+ self.get_logger().info("Skill/Primitive Specs: Received")
+ elif key == "skill_specs":
+ skill_specs = json.loads(request.params.values[i])
+ skill_count = (
+ len(skill_specs)
+ if isinstance(skill_specs, list)
+ else "unknown"
+ )
+ self.get_logger().info(
+ f"Skill Specs: Found {skill_count} skills"
+ )
+ if skill_specs:
+ self.get_logger().info(
+ f"Available Skills:\n{json.dumps(skill_specs, indent=2)}"
+ )
+ except Exception as e:
+ self.get_logger().warn(
+ f"Failed to parse {key} from params: {e}"
+ )
+
+ # Generate RTDL code
+ self.get_logger().info("-" * 80)
+ self.get_logger().info("Generating RTDL code...")
+ rtdl_code = self._generate_rtdl(
+ request.description,
+ object_graph,
+ rtdl_syntax,
+ skill_primitive_specs,
+ skill_specs,
+ )
+
+ response.rtdl = rtdl_code
+ response.rtdl_type = "list" # Simple list-style RTDL
+ response.stamp = self.get_clock().now().to_msg()
+
+ self.get_logger().info("-" * 80)
+ self.get_logger().info(
+ f"Generated RTDL (type: {response.rtdl_type}, length: {len(rtdl_code)} chars)"
+ )
+ self.get_logger().info(f"RTDL Code:\n{rtdl_code}")
+ self.get_logger().info("=" * 80)
+ return response
+
+ def _generate_rtdl(
+ self,
+ description,
+ object_graph=None,
+ rtdl_syntax=None,
+ skill_primitive_specs=None,
+ skill_specs=None,
+ ):
+ """
+ Generate simple list-style RTDL code from description using Qwen LLM.
+ Format: JSON array of instructions, each with type, name, and params.
+ """
+ if not self.qwen_client:
+ raise RuntimeError("Qwen client not initialized")
+
+ return self._generate_rtdl_with_qwen(
+ description, object_graph, rtdl_syntax, skill_primitive_specs, skill_specs
+ )
+
+ def _generate_rtdl_with_qwen(
+ self,
+ description,
+ object_graph=None,
+ rtdl_syntax=None,
+ skill_primitive_specs=None,
+ skill_specs=None,
+ ):
+ """Generate RTDL using Qwen LLM API (DashScope)."""
+ self.get_logger().info(f"Using Qwen LLM ({self.qwen_model}) for task planning")
+
+ # Build system prompt with RTDL syntax if provided
+ if rtdl_syntax:
+ system_prompt = f"""You are a robot task planning expert. Your task is to convert natural language task descriptions into RTDL (Robot Task Description Langauge) code.
+
+RTDL Syntax Specification:
+{json.dumps(rtdl_syntax, indent=2)}
+
+Important:
+- Only output valid JSON, no additional text
+- Follow the RTDL syntax specification exactly
+- ALWAYS include "object_id" field in each instruction (required to specify which object executes the instruction)
+- Use object_id from the object graph (usually the robot object)
+- Use skills/primitives available in the specified object's registered_skills/registered_primitives
+- When task description mentions an object, find the best matching object from object_graph by label
+- Use the object's 'id' (UUID) to reference it in params - this is the unique identifier
+- Labels may not exactly match (e.g., 'plant' vs 'potted plant') - use semantic understanding to find the best match
+- Keep instructions simple and sequential"""
+ else:
+ # Default RTDL format
+ system_prompt = """You are a robot task planning expert. Your task is to convert natural language task descriptions into RTDL (Robot Task Description Langauge) code.
+
+RTDL Format:
+- Output a JSON array of instructions
+- Each instruction is a JSON object with:
+ - "object_id": string (REQUIRED) - ID of the object that executes this instruction (usually robot, e.g., "robot_001")
+ - "type": "skill" or "primitive"
+ - "name": skill/primitive name (e.g., "skl::pick", "skl::place", "skl::navigate", "prm::arm.move.ee") # remember to prefix with "skl::" or "prm::"
+ - "params": JSON object with skill/primitive parameters
+
+Example:
+[
+ {"object_id": "robot_001", "type": "skill", "name": "skl::pick", "params": {"target": "cup_001"}},
+ {"object_id": "robot_001", "type": "skill", "name": "skl::place", "params": {"target": "cup_001", "destination": "table_001"}}
+]
+
+Important:
+- Only output valid JSON, no additional text
+- ALWAYS include "object_id" field in each instruction (required)
+- Use object_id from the object graph (usually the robot object)
+- Use skills/primitives available in the specified object's registered_skills/registered_primitives
+- When task description mentions an object, find the best matching object from object_graph by label
+- Use the object's 'id' (UUID) to reference it in params - this is the unique identifier
+- Labels may not exactly match (e.g., 'plant' vs 'potted plant') - use semantic understanding
+- Keep instructions simple and sequential"""
+
+ user_prompt = f"Task description: {description}\n\n"
+
+ # Add object graph information
+ if object_graph:
+ user_prompt += f"Available objects in environment:\n{json.dumps(object_graph, indent=2)}\n\n"
+
+ # CRITICAL: Object selection instructions
+ user_prompt += "=" * 80 + "\n"
+ user_prompt += "OBJECT SELECTION RULES:\n"
+ user_prompt += "=" * 80 + "\n"
+ user_prompt += (
+ "1. Each object has a unique 'id' (UUID) and a 'label' (name).\n"
+ )
+ user_prompt += "2. When the task description mentions an object (e.g., 'go to the plant at corner'):\n"
+ user_prompt += " - Find the BEST MATCHING object by comparing the description with object labels\n"
+ user_prompt += " - Labels may not exactly match (e.g., task says 'plant' but object is 'potted plant')\n"
+ user_prompt += (
+ " - Use semantic understanding to find the most relevant object\n"
+ )
+ user_prompt += (
+ " - Consider location hints (e.g., 'at corner', 'on the table')\n"
+ )
+ user_prompt += "3. To reference an object in RTDL params, use BOTH:\n"
+ user_prompt += " - The object's 'id' (UUID) as the unique identifier\n"
+ user_prompt += " - The object's 'label' for human readability\n"
+ user_prompt += (
+ "4. Example: If task is 'go to the plant' and object graph has:\n"
+ )
+ user_prompt += ' {"id": "abc123", "label": "potted plant", ...}\n'
+ user_prompt += ' Then in RTDL params, use: {"target_object_id": "abc123", "target_object_label": "potted plant"}\n'
+ user_prompt += (
+ ' OR if the skill accepts just an ID: {"target": "abc123"}\n'
+ )
+ user_prompt += "5. The 'id' (UUID) is the UNIQUE identifier - always use it to specify which object to interact with.\n"
+ user_prompt += "=" * 80 + "\n\n"
+
+ # Extract robot object and its skills/primitives
+ robot_objects = [
+ obj
+ for obj in object_graph
+ if "robot" in obj.get("label", "").lower()
+ or "robot" in obj.get("id", "").lower()
+ or obj.get("id") == "robot_self"
+ ]
+ if robot_objects:
+ robot = robot_objects[0]
+ robot_id = robot.get("id", "robot_self")
+ robot_skills = robot.get("registered_skills", [])
+ robot_primitives = robot.get("registered_primitives", [])
+ user_prompt += f"Robot object ID: {robot_id}\n"
+ user_prompt += f"Robot registered skills: {robot_skills}\n"
+ user_prompt += f"Robot registered primitives: {robot_primitives}\n"
+ user_prompt += f"IMPORTANT: Use object_id='{robot_id}' in all RTDL instructions to specify that this robot executes them.\n"
+ else:
+ user_prompt += "WARNING: No robot object found in object graph. You must still include 'object_id' field in each instruction.\n"
+
+ user_prompt += "\n"
+ user_prompt += "When the task description mentions an object:\n"
+ user_prompt += "- Search through the object graph to find the best matching object by label\n"
+ user_prompt += (
+ "- Use the object's 'id' (UUID) to reference it in RTDL params\n"
+ )
+ user_prompt += "- If multiple objects match, choose based on context (location, description hints)\n"
+ user_prompt += "\n"
+
+ # Add skill and primitive specifications
+ if skill_primitive_specs:
+ user_prompt += f"\nAvailable Skills and Primitives:\n{json.dumps(skill_primitive_specs, indent=2)}\n\n"
+ user_prompt += "When generating RTDL, use the input/output parameter types from the specifications above.\n"
+ user_prompt += "Ensure all parameters match the expected types (string, float, bool, geometry_msgs/msg/PoseStamped, etc.).\n"
+
+ # Add skill specs from system API (preferred over skill_primitive_specs)
+ if skill_specs:
+ user_prompt += f"\nAvailable Skills (from system API):\n{json.dumps(skill_specs, indent=2)}\n\n"
+ user_prompt += "=" * 80 + "\n"
+ user_prompt += "SKILL USAGE INSTRUCTIONS:\n"
+ user_prompt += "=" * 80 + "\n"
+ user_prompt += (
+ "IMPORTANT: Use ONLY the skills listed above. Each skill has:\n"
+ )
+ user_prompt += "- name: The skill name (e.g., 'skl::wandering', 'skl::move_to_object')\n"
+ user_prompt += "- start_args: The input parameter schema (JSON object describing required/optional parameters)\n"
+ user_prompt += (
+ " * This schema tells you what parameters the skill expects\n"
+ )
+ user_prompt += " * Required parameters MUST be included in 'params'\n"
+ user_prompt += " * Optional parameters can be included if needed\n"
+ user_prompt += "- start_topic: The topic to publish to start the skill\n"
+ user_prompt += "\n"
+ user_prompt += "When generating RTDL:\n"
+ user_prompt += "1. Use the exact skill name from the 'name' field (e.g., 'skl::wandering', 'skl::move_to_object')\n"
+ user_prompt += "2. Read the 'start_args' schema carefully to understand what parameters the skill expects\n"
+ user_prompt += (
+ "3. For object-related skills (e.g., 'skl::move_to_object'):\n"
+ )
+ user_prompt += " - Use 'target_object_id' parameter with the object's UUID (from object_graph)\n"
+ user_prompt += " - The UUID is the unique identifier for the object\n"
+ user_prompt += "4. Ensure all required parameters in 'params' match the types specified in 'start_args'\n"
+ user_prompt += "5. Only use skills that are listed above\n"
+ user_prompt += "=" * 80 + "\n\n"
+
+ user_prompt += "\nGenerate RTDL code for this task:"
+
+ # Log the full prompt
+ self.get_logger().info("-" * 80)
+ self.get_logger().info("Qwen LLM Prompt:")
+ self.get_logger().info("System Prompt:")
+ self.get_logger().info(system_prompt)
+ self.get_logger().info("User Prompt:")
+ self.get_logger().info(user_prompt)
+ self.get_logger().info("-" * 80)
+
+ # Call Qwen API
+ import time
+
+ start_time = time.time()
+ self.get_logger().info("Calling Qwen API...")
+ response = self.qwen_client.chat.completions.create(
+ model=self.qwen_model,
+ messages=[
+ {"role": "system", "content": system_prompt},
+ {"role": "user", "content": user_prompt},
+ ],
+ temperature=0.1,
+ )
+ elapsed_time = time.time() - start_time
+
+ if not response.choices or len(response.choices) == 0:
+ raise RuntimeError("Qwen API response has no choices")
+
+ message_content = response.choices[0].message.content
+ if message_content is None:
+ raise RuntimeError("Qwen API response content is None")
+
+ llm_response = message_content.strip()
+ self.get_logger().info(
+ f"Qwen API call completed in {elapsed_time:.2f} seconds"
+ )
+ self.get_logger().info("-" * 80)
+ self.get_logger().info("Qwen LLM Response (full):")
+ self.get_logger().info(llm_response)
+ self.get_logger().info("-" * 80)
+
+ # Try to extract JSON from response (might have markdown code blocks)
+ if llm_response.startswith("```"):
+ # Remove markdown code blocks
+ lines = llm_response.split("\n")
+ json_start = None
+ json_end = None
+ for i, line in enumerate(lines):
+ if line.strip().startswith("```"):
+ if json_start is None:
+ json_start = i + 1
+ else:
+ json_end = i
+ break
+ if json_start and json_end:
+ llm_response = "\n".join(lines[json_start:json_end])
+
+ # Parse JSON response
+ try:
+ instructions = json.loads(llm_response)
+ if not isinstance(instructions, list):
+ instructions = [instructions]
+
+ # Validate and format instructions
+ formatted_instructions = []
+ for inst in instructions:
+ if isinstance(inst, dict):
+ inst_object_id = inst.get(
+ "object_id", "robot_001"
+ ) # Default to robot_001 if missing
+ inst_type = inst.get("type", "skill")
+ inst_name = inst.get("name", "unknown")
+ inst_params = inst.get("params", {})
+
+ # Validate object IDs in params (for skills like move_to_object)
+ if (
+ inst_name == "skl::move_to_object"
+ and "target_object_id" in inst_params
+ ):
+ target_id = inst_params["target_object_id"]
+ # Verify the object ID exists in object_graph
+ if object_graph:
+ object_ids = [
+ obj.get("id")
+ for obj in object_graph
+ if isinstance(obj, dict)
+ ]
+ if target_id not in object_ids:
+ self.get_logger().warn(
+ f'Generated RTDL references object ID "{target_id}" which is not in object_graph. '
+ f"Available IDs: {object_ids[:10]}{'...' if len(object_ids) > 10 else ''}"
+ )
+
+ formatted_instructions.append(
+ {
+ "object_id": inst_object_id, # REQUIRED: object that executes this instruction
+ "type": inst_type,
+ "name": inst_name,
+ "params": inst_params,
+ }
+ )
+
+ # Log the generated RTDL for debugging
+ self.get_logger().info("Generated RTDL instructions:")
+ for idx, inst in enumerate(formatted_instructions):
+ self.get_logger().info(
+ f" [{idx}] {inst.get('type')} {inst.get('name')} with params: {inst.get('params')}"
+ )
+
+ # Convert to JSON string (RTDL format)
+ return json.dumps(formatted_instructions, indent=2)
+ except json.JSONDecodeError as e:
+ self.get_logger().error(f"Failed to parse Qwen response as JSON: {e}")
+ self.get_logger().error(f"Response was: {llm_response}")
+ raise RuntimeError(f"Failed to parse Qwen response as JSON: {e}")
+
+ def _validate_api_key(self, api_key: str) -> bool:
+ """
+ Validate DashScope/Qwen API key format.
+ API keys typically start with 'sk-' and are at least 32 characters long.
+ """
+ if not api_key or not isinstance(api_key, str):
+ return False
+
+ # Remove whitespace
+ api_key = api_key.strip()
+
+ # Check minimum length (DashScope API keys are typically 32+ characters, including 'sk-' prefix)
+ # Minimum: 'sk-' (3) + key part (32) = 35 characters
+ if len(api_key) < 35:
+ return False
+
+ # Check if starts with 'sk-' (common format for API keys)
+ if not api_key.startswith("sk-"):
+ return False
+
+ # Check if contains only valid characters (alphanumeric, hyphens, underscores)
+ # After 'sk-' prefix
+ key_part = api_key[3:]
+ if not key_part.replace("-", "").replace("_", "").isalnum():
+ return False
+
+ # Check that key part has reasonable length (at least 32 characters)
+ if len(key_part) < 32:
+ return False
+
+ return True
+
+
+def main(args=None):
+ rclpy.init(args=args)
+ task_plan_service = TaskPlanService()
+ rclpy.spin(task_plan_service)
+ task_plan_service.destroy_node()
+ rclpy.shutdown()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/rust/provider/demo_service/demo_service_provider/transform_scan_service.py b/rust/provider/demo_service/demo_service_provider/transform_scan_service.py
new file mode 100644
index 0000000..4bd1ab2
--- /dev/null
+++ b/rust/provider/demo_service/demo_service_provider/transform_scan_service.py
@@ -0,0 +1,195 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: MulanPSL-2.0
+# Transform Scan Service
+#
+# Serves many callers: single-call CONVERT or per-client stream. Each stream client
+# gets a dedicated output_topic and stream_id; STOP_STREAM releases resources.
+
+import math
+import time
+import uuid
+import numpy as np
+import rclpy
+from rclpy.node import Node
+from sensor_msgs.msg import PointCloud2, LaserScan
+from sensor_msgs_py import point_cloud2
+from robonix_sdk.srv import TransformScan
+
+ACTION_CONVERT = 0
+ACTION_START_STREAM = 1
+ACTION_STOP_STREAM = 2
+
+STREAM_NS = "/demo_service/transform_scan/stream"
+
+
+def pointcloud_to_laserscan(cloud: PointCloud2, angle_min=-math.pi, angle_max=math.pi,
+ num_bins=360, range_min=0.1, range_max=10.0) -> LaserScan | None:
+ """Convert PointCloud2 to LaserScan by projecting to 2D and binning by angle (min range per bin)."""
+ try:
+ points = point_cloud2.read_points(
+ cloud, field_names=["x", "y", "z"], skip_nans=True
+ )
+ pts = np.array(list(points), dtype=np.float64)
+ except Exception:
+ return None
+ if pts.size == 0:
+ return None
+ x, y = pts[:, 0], pts[:, 1]
+ ranges = np.sqrt(x * x + y * y)
+ angles = np.arctan2(y, x)
+ angle_inc = (angle_max - angle_min) / num_bins
+ out = LaserScan()
+ out.header = cloud.header
+ out.angle_min = angle_min
+ out.angle_max = angle_max
+ out.angle_increment = angle_inc
+ out.time_increment = 0.0
+ out.scan_time = 0.0
+ out.range_min = range_min
+ out.range_max = range_max
+ out.ranges = [float("inf")] * num_bins
+ for i in range(num_bins):
+ a_lo = angle_min + i * angle_inc
+ a_hi = angle_min + (i + 1) * angle_inc
+ mask = (angles >= a_lo) & (angles < a_hi) & (ranges >= range_min) & (ranges <= range_max)
+ if np.any(mask):
+ out.ranges[i] = float(np.min(ranges[mask]))
+ return out
+
+
+class StreamSlot:
+ """One stream client: sub to input_topic, pub to dedicated output_topic."""
+
+ def __init__(self, node: Node, stream_id: str, input_topic: str):
+ self.stream_id = stream_id
+ self.input_topic = input_topic
+ self.output_topic = f"{STREAM_NS}/{stream_id}"
+ self._node = node
+ self._pub = node.create_publisher(LaserScan, self.output_topic, 10)
+ self._sub = node.create_subscription(
+ PointCloud2, input_topic, self._on_cloud, 10
+ )
+
+ def _on_cloud(self, msg: PointCloud2):
+ scan = pointcloud_to_laserscan(msg)
+ if scan is not None:
+ self._pub.publish(scan)
+
+ def destroy(self):
+ self._node.destroy_publisher(self._pub)
+ self._node.destroy_subscription(self._sub)
+
+
+class TransformScanService(Node):
+ """Transform scan: CONVERT (one shot), START_STREAM (per-client output_topic), STOP_STREAM (release)."""
+
+ def __init__(self):
+ super().__init__("demo_transform_scan_service")
+ self._streams = {} # stream_id -> StreamSlot
+ self._latest_cloud = None
+ self._single_sub = None
+ self._single_topic = None
+ self.srv = self.create_service(
+ TransformScan, "/demo_service/transform_scan/convert", self.srv_callback
+ )
+ self.get_logger().info(
+ "Transform scan service: /demo_service/transform_scan/convert "
+ "(action 0=CONVERT 1=START_STREAM 2=STOP_STREAM)"
+ )
+
+ def srv_callback(self, request, response):
+ response.success = False
+ response.scan = LaserScan()
+ response.output_topic = ""
+ response.stream_id = ""
+
+ if request.action == ACTION_CONVERT:
+ return self._do_convert(request, response)
+ if request.action == ACTION_START_STREAM:
+ return self._do_start_stream(request, response)
+ if request.action == ACTION_STOP_STREAM:
+ return self._do_stop_stream(request, response)
+
+ self.get_logger().warn(f"Unknown action {request.action}")
+ return response
+
+ def _do_convert(self, request, response):
+ cloud = None
+ if request.input_topic and request.input_topic.strip():
+ topic = request.input_topic.strip()
+ if self._single_topic != topic:
+ if self._single_sub is not None:
+ self.destroy_subscription(self._single_sub)
+ self._single_sub = None
+ self._latest_cloud = None
+ self._single_topic = topic
+ self._single_sub = self.create_subscription(
+ PointCloud2, topic, self._cloud_cb, 10
+ )
+ deadline = time.monotonic() + 5.0
+ while self._latest_cloud is None and time.monotonic() < deadline:
+ rclpy.spin_once(self, timeout_sec=0.1)
+ cloud = self._latest_cloud
+ else:
+ if request.pointcloud.width * request.pointcloud.height > 0:
+ cloud = request.pointcloud
+ if cloud is None:
+ return response
+ scan = pointcloud_to_laserscan(cloud)
+ if scan is None:
+ return response
+ response.success = True
+ response.scan = scan
+ return response
+
+ def _do_start_stream(self, request, response):
+ input_topic = (request.input_topic or "").strip()
+ if not input_topic:
+ self.get_logger().warn("START_STREAM requires input_topic")
+ return response
+ stream_id = str(uuid.uuid4())
+ try:
+ slot = StreamSlot(self, stream_id, input_topic)
+ self._streams[stream_id] = slot
+ response.success = True
+ response.output_topic = slot.output_topic
+ response.stream_id = stream_id
+ self.get_logger().info(
+ f"START_STREAM stream_id={stream_id} input={input_topic} output={slot.output_topic}"
+ )
+ except Exception as e:
+ self.get_logger().error(f"START_STREAM failed: {e}")
+ return response
+
+ def _do_stop_stream(self, request, response):
+ stream_id = (request.stream_id or "").strip()
+ if not stream_id:
+ self.get_logger().warn("STOP_STREAM requires stream_id")
+ return response
+ slot = self._streams.pop(stream_id, None)
+ if slot is None:
+ self.get_logger().warn(f"STOP_STREAM unknown stream_id={stream_id}")
+ return response
+ slot.destroy()
+ response.success = True
+ self.get_logger().info(f"STOP_STREAM released stream_id={stream_id}")
+ return response
+
+ def _cloud_cb(self, msg):
+ self._latest_cloud = msg
+
+
+def main(args=None):
+ rclpy.init(args=args)
+ node = TransformScanService()
+ try:
+ rclpy.spin(node)
+ finally:
+ for slot in list(node._streams.values()):
+ slot.destroy()
+ node.destroy_node()
+ rclpy.shutdown()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/rust/provider/demo_package_service/package.xml b/rust/provider/demo_service/package.xml
similarity index 91%
rename from rust/provider/demo_package_service/package.xml
rename to rust/provider/demo_service/package.xml
index 9492f39..8056df0 100644
--- a/rust/provider/demo_package_service/package.xml
+++ b/rust/provider/demo_service/package.xml
@@ -11,6 +11,8 @@
std_msgs
builtin_interfaces
robonix_sdk
+ sensor_msgs
+ sensor_msgs_py
ament_copyright
ament_flake8
diff --git a/rust/provider/demo_package_service/rbnx/build.sh b/rust/provider/demo_service/rbnx/build.sh
similarity index 98%
rename from rust/provider/demo_package_service/rbnx/build.sh
rename to rust/provider/demo_service/rbnx/build.sh
index ad60bc1..a32d3e5 100755
--- a/rust/provider/demo_package_service/rbnx/build.sh
+++ b/rust/provider/demo_service/rbnx/build.sh
@@ -71,5 +71,7 @@ else
exit 1
fi
+# sudo apt install ros-humble-rmw-cyclonedds-cpp
+
echo "Build completed successfully!"
diff --git a/rust/provider/demo_service/rbnx/configs/building_map_config.yaml b/rust/provider/demo_service/rbnx/configs/building_map_config.yaml
new file mode 100644
index 0000000..6864759
--- /dev/null
+++ b/rust/provider/demo_service/rbnx/configs/building_map_config.yaml
@@ -0,0 +1,27 @@
+manual_objects:
+ - label: kitchen_target_waypoint
+ frame_mapping:
+ - frame_id: "map"
+ center:
+ x: 8.21899
+ y: -11.6421
+ z: 0.0
+ bbox:
+ - scale_x: 0.1
+ scale_y: 0.1
+ scale_z: 0.1
+ yaw: 0.0
+ - label: room_315_target_waypoint
+ frame_mapping:
+ - frame_id: "map"
+ center:
+ x: 1.79997
+ y: 0.311725
+ z: 0.0
+ bbox:
+ - scale_x: 0.1
+ scale_y: 0.1
+ scale_z: 0.1
+ yaw: 0.0
+
+# Setting goal pose: Frame:map, Position(-0.284224, 0.199099, 0), Orientation(0, 0, 0.92859, 0.371106) = Angle: 2.38119
\ No newline at end of file
diff --git a/rust/provider/demo_service/rbnx/configs/demo_map_config.yaml b/rust/provider/demo_service/rbnx/configs/demo_map_config.yaml
new file mode 100644
index 0000000..934070f
--- /dev/null
+++ b/rust/provider/demo_service/rbnx/configs/demo_map_config.yaml
@@ -0,0 +1,28 @@
+manual_objects:
+ - label: room_340_target_waypoint
+ frame_mapping:
+ - frame_id: "map"
+ center:
+ x: -38.5283
+ y: -10.4608
+ z: 0.0
+ bbox:
+ - scale_x: 0.1
+ scale_y: 0.1
+ scale_z: 0.1
+ yaw: 1.6405
+ - label: vending_machine_target_waypoint
+ frame_mapping:
+ - frame_id: "map"
+ center:
+ x: -26.8631
+ y: -13.2171
+ z: 0.0
+ bbox:
+ - scale_x: 0.1
+ scale_y: 0.1
+ scale_z: 0.1
+ yaw: 1.75626
+
+# Position(-38.5283,-10.4608,0). Orientation(0, 0.731315, 0.68204) = Angle: 1.6405 - room340_waypoint
+# Position(-26.8631, -13.2171, 0), Orientation (0, 0, 0.769545, 0.638593) = Angle: 1.75626 - vending_machine_waypoint
\ No newline at end of file
diff --git a/rust/provider/demo_service/rbnx/configs/webots_map_config.yaml b/rust/provider/demo_service/rbnx/configs/webots_map_config.yaml
new file mode 100644
index 0000000..eabe973
--- /dev/null
+++ b/rust/provider/demo_service/rbnx/configs/webots_map_config.yaml
@@ -0,0 +1,26 @@
+# Webots simulator semantic map config
+manual_objects:
+ - label: init_waypoint
+ frame_mapping:
+ - frame_id: "map"
+ center:
+ x: -0.284224
+ y: 0.199099
+ z: 0.0
+ bbox:
+ - scale_x: 0.1
+ scale_y: 0.1
+ scale_z: 0.1
+ yaw: 0.0
+ - label: door_waypoint
+ frame_mapping:
+ - frame_id: "map"
+ center:
+ x: -9.49493
+ y: 3.14404
+ z: 0.0
+ bbox:
+ - scale_x: 0.1
+ scale_y: 0.1
+ scale_z: 0.1
+ yaw: 0.0
diff --git a/rust/provider/demo_service/rbnx/start_semantic_map.sh b/rust/provider/demo_service/rbnx/start_semantic_map.sh
new file mode 100755
index 0000000..cb10c83
--- /dev/null
+++ b/rust/provider/demo_service/rbnx/start_semantic_map.sh
@@ -0,0 +1,15 @@
+#!/bin/bash
+# SPDX-License-Identifier: MulanPSL-2.0
+# Start semantic map service. Optional: pass config name to select map config.
+# Usage: ./start_semantic_map.sh [config]
+# config: building (default), webots, or a filename e.g. my_map_config.yaml
+set -e
+CONFIG="${HOME:-/tmp}/.robonix/config.yaml"
+[ -z "$ROBONIX_SDK_PATH" ] && [ -f "$CONFIG" ] && \
+ ROBONIX_SDK_PATH=$(grep 'robonix_sdk_path' "$CONFIG" 2>/dev/null | sed 's/.*:[[:space:]]*//;s/[[:space:]]*$//' | tr -d "\"'")
+[ -n "$ROBONIX_SDK_PATH" ] && { [ -f "$ROBONIX_SDK_PATH/install/setup.bash" ] && source "$ROBONIX_SDK_PATH/install/setup.bash"; _d=$(find "$ROBONIX_SDK_PATH/install" -type d -path "*/lib/python*/site-packages" 2>/dev/null | head -1); [ -n "$_d" ] && export PYTHONPATH="${_d}:${PYTHONPATH}"; }
+[ -f /opt/ros/humble/setup.bash ] && source /opt/ros/humble/setup.bash
+cd "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" && export PYTHONPATH="${PWD}:${PYTHONPATH}"
+CONFIG_ARG=""
+[ -n "$1" ] && CONFIG_ARG="--config $1"
+exec python3 -m demo_service_provider.semantic_map_service $CONFIG_ARG
diff --git a/rust/provider/demo_service/rbnx/start_task_plan.sh b/rust/provider/demo_service/rbnx/start_task_plan.sh
new file mode 100755
index 0000000..ea18ae8
--- /dev/null
+++ b/rust/provider/demo_service/rbnx/start_task_plan.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+# SPDX-License-Identifier: MulanPSL-2.0
+set -e
+CONFIG="${HOME:-/tmp}/.robonix/config.yaml"
+[ -z "$ROBONIX_SDK_PATH" ] && [ -f "$CONFIG" ] && \
+ ROBONIX_SDK_PATH=$(grep 'robonix_sdk_path' "$CONFIG" 2>/dev/null | sed 's/.*:[[:space:]]*//;s/[[:space:]]*$//' | tr -d "\"'")
+[ -n "$ROBONIX_SDK_PATH" ] && { [ -f "$ROBONIX_SDK_PATH/install/setup.bash" ] && source "$ROBONIX_SDK_PATH/install/setup.bash"; _d=$(find "$ROBONIX_SDK_PATH/install" -type d -path "*/lib/python*/site-packages" 2>/dev/null | head -1); [ -n "$_d" ] && export PYTHONPATH="${_d}:${PYTHONPATH}"; }
+[ -f /opt/ros/humble/setup.bash ] && source /opt/ros/humble/setup.bash
+cd "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" && export PYTHONPATH="${PWD}:${PYTHONPATH}"
+exec python3 -m demo_service_provider.task_plan_service
diff --git a/rust/provider/demo_service/rbnx/start_transform_scan.sh b/rust/provider/demo_service/rbnx/start_transform_scan.sh
new file mode 100755
index 0000000..0e73354
--- /dev/null
+++ b/rust/provider/demo_service/rbnx/start_transform_scan.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+# SPDX-License-Identifier: MulanPSL-2.0
+set -e
+CONFIG="${HOME:-/tmp}/.robonix/config.yaml"
+[ -z "$ROBONIX_SDK_PATH" ] && [ -f "$CONFIG" ] && \
+ ROBONIX_SDK_PATH=$(grep 'robonix_sdk_path' "$CONFIG" 2>/dev/null | sed 's/.*:[[:space:]]*//;s/[[:space:]]*$//' | tr -d "\"'")
+[ -n "$ROBONIX_SDK_PATH" ] && { [ -f "$ROBONIX_SDK_PATH/install/setup.bash" ] && source "$ROBONIX_SDK_PATH/install/setup.bash"; _d=$(find "$ROBONIX_SDK_PATH/install" -type d -path "*/lib/python*/site-packages" 2>/dev/null | head -1); [ -n "$_d" ] && export PYTHONPATH="${_d}:${PYTHONPATH}"; }
+[ -f /opt/ros/humble/setup.bash ] && source /opt/ros/humble/setup.bash
+cd "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" && export PYTHONPATH="${PWD}:${PYTHONPATH}"
+exec python3 -m demo_service_provider.transform_scan_service
diff --git a/rust/provider/demo_package_service/rbnx/stop_semantic_map.sh b/rust/provider/demo_service/rbnx/stop_semantic_map.sh
similarity index 100%
rename from rust/provider/demo_package_service/rbnx/stop_semantic_map.sh
rename to rust/provider/demo_service/rbnx/stop_semantic_map.sh
diff --git a/rust/provider/demo_package_service/rbnx/stop_task_plan.sh b/rust/provider/demo_service/rbnx/stop_task_plan.sh
similarity index 100%
rename from rust/provider/demo_package_service/rbnx/stop_task_plan.sh
rename to rust/provider/demo_service/rbnx/stop_task_plan.sh
diff --git a/rust/provider/demo_service/rbnx/stop_transform_scan.sh b/rust/provider/demo_service/rbnx/stop_transform_scan.sh
new file mode 100755
index 0000000..4b9a3c3
--- /dev/null
+++ b/rust/provider/demo_service/rbnx/stop_transform_scan.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+# SPDX-License-Identifier: MulanPSL-2.0
+set -e
+echo "Stopping transform_scan service..."
diff --git a/rust/provider/demo_package_service/rbnx_manifest.yaml b/rust/provider/demo_service/rbnx_manifest.yaml
similarity index 62%
rename from rust/provider/demo_package_service/rbnx_manifest.yaml
rename to rust/provider/demo_service/rbnx_manifest.yaml
index 514ee6e..510de9d 100644
--- a/rust/provider/demo_package_service/rbnx_manifest.yaml
+++ b/rust/provider/demo_service/rbnx_manifest.yaml
@@ -11,23 +11,23 @@ package:
name: demo_service_provider
version: 0.0.1
description: Demo package providing semantic_map and task_plan services
- maintainer: root
- maintainer_email: demo@demo.demo
+ maintainer: wheatfox
+ maintainer_email: wheatfox17@icloud.com
license: MulanPSL-2.0
# build_script: rbnx/build.sh # Optional: build script path. If omitted, defaults to rbnx/build.sh
# Services provided by this package
# Services provide standardized algorithm capabilities
services:
- - name: semantic_map
+ - name: srv::semantic_map
srv_type: robonix_sdk/srv/service/semantic_map/QuerySemanticMap
entry: /demo_service/semantic_map/query
- metadata: '{"model":"demo","backend":"simulated"}'
+ metadata: '{"model":"qwen3-vl-plus","backend":"vlm","depth_camera":"prm::camera.depth","rgb_camera":"prm::camera.rgb","pose":"prm::base.pose.cov"}'
version: 0.0.1
- start_script: rbnx/start_semantic_map.sh
+ start_script: rbnx/start_semantic_map.sh configs/building_map_config.yaml
stop_script: rbnx/stop_semantic_map.sh
- - name: task_plan
+ - name: srv::task_plan
srv_type: robonix_sdk/srv/service/task_plan/PlanTask
entry: /demo_service/task_plan/plan
metadata: '{"model":"demo","capabilities":["navigation","manipulation"],"rtdl_type":"list"}'
@@ -35,3 +35,10 @@ services:
start_script: rbnx/start_task_plan.sh
stop_script: rbnx/stop_task_plan.sh
+ - name: srv::transform.scan
+ srv_type: robonix_sdk/srv/service/transform_scan/TransformScan
+ entry: /demo_service/transform_scan/convert
+ metadata: '{"input":"pointcloud_or_topic","output":"LaserScan","stream":"use prm::trasform.laserscan"}'
+ version: 0.0.1
+ start_script: rbnx/start_transform_scan.sh
+ stop_script: rbnx/stop_transform_scan.sh
diff --git a/rust/provider/demo_package_service/resource/demo_service_provider b/rust/provider/demo_service/resource/demo_service_provider
similarity index 100%
rename from rust/provider/demo_package_service/resource/demo_service_provider
rename to rust/provider/demo_service/resource/demo_service_provider
diff --git a/rust/provider/demo_package_service/setup.py b/rust/provider/demo_service/setup.py
similarity index 87%
rename from rust/provider/demo_package_service/setup.py
rename to rust/provider/demo_service/setup.py
index 20a62c6..7734c6e 100644
--- a/rust/provider/demo_package_service/setup.py
+++ b/rust/provider/demo_service/setup.py
@@ -20,6 +20,9 @@
'setuptools',
'python-dotenv',
'openai>=1.0.0',
+ 'cv-bridge',
+ 'numpy',
+ 'Pillow',
],
zip_safe=True,
maintainer='root',
@@ -30,6 +33,7 @@
'console_scripts': [
'semantic_map_service = demo_service_provider.semantic_map_service:main',
'task_plan_service = demo_service_provider.task_plan_service:main',
+ 'transform_scan_service = demo_service_provider.transform_scan_service:main',
],
},
)
diff --git a/rust/provider/navigation_skills/navigation_skills_provider/__init__.py b/rust/provider/navigation_skills/navigation_skills_provider/__init__.py
new file mode 100755
index 0000000..788b6a9
--- /dev/null
+++ b/rust/provider/navigation_skills/navigation_skills_provider/__init__.py
@@ -0,0 +1 @@
+# Navigation Skills Provider Package
diff --git a/rust/provider/navigation_skills/navigation_skills_provider/move_to_object_skill.py b/rust/provider/navigation_skills/navigation_skills_provider/move_to_object_skill.py
new file mode 100755
index 0000000..f4f3a57
--- /dev/null
+++ b/rust/provider/navigation_skills/navigation_skills_provider/move_to_object_skill.py
@@ -0,0 +1,1037 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: MulanPSL-2.0
+# Move to Object Skill
+#
+# Skill to navigate to a specific object nearby.
+# Uses semantic_map service to find the object and navigates to it.
+""""""
+
+import rclpy
+from rclpy.node import Node
+from rclpy.qos import (
+ QoSProfile,
+ ReliabilityPolicy,
+ HistoryPolicy,
+ DurabilityPolicy,
+ LivelinessPolicy,
+)
+from rclpy.duration import Duration
+from std_msgs.msg import String, Bool
+from geometry_msgs.msg import PoseStamped, PoseWithCovarianceStamped
+import json
+import time
+import signal
+import math
+from robonix_sdk.srv import QuerySemanticMap, QueryPrimitive, QueryService
+
+
+class MoveToObjectSkill(Node):
+ """Implements move_to_object skill that navigates to a specific object."""
+
+ def __init__(self):
+ super().__init__("move_to_object_skill")
+
+ self.start_topic = "/robot1/skill/move_to_object/start"
+ self.status_topic = "/robot1/skill/move_to_object/status"
+
+ self.semantic_map_service_entry = None
+ self.pose_topic = None
+ self.navigate_goal_topic = None
+ self.navigate_status_topic = None
+
+ service_qos = QoSProfile(
+ reliability=ReliabilityPolicy.RELIABLE,
+ history=HistoryPolicy.KEEP_LAST,
+ depth=10,
+ durability=DurabilityPolicy.VOLATILE,
+ )
+ service_qos.deadline = Duration(seconds=0)
+ service_qos.lifespan = Duration(seconds=0)
+ service_qos.liveliness = LivelinessPolicy.AUTOMATIC
+ service_qos.liveliness_lease_duration = Duration(seconds=0)
+
+ self.query_primitive_client = self.create_client(
+ QueryPrimitive, "/rbnx/prm/query", qos_profile=service_qos
+ )
+ self.get_logger().info("QueryPrimitive service client created")
+
+ self.query_service_client = self.create_client(
+ QueryService, "/rbnx/srv/query", qos_profile=service_qos
+ )
+ self.get_logger().info("QueryService service client created")
+
+ self._query_services_and_primitives()
+
+ start_topic_qos = QoSProfile(
+ reliability=ReliabilityPolicy.RELIABLE,
+ history=HistoryPolicy.KEEP_LAST,
+ depth=10,
+ durability=DurabilityPolicy.VOLATILE,
+ )
+ self.start_subscriber = self.create_subscription(
+ String, self.start_topic, self.start_callback, start_topic_qos
+ )
+ self.get_logger().info(
+ f"Subscribing to start topic: {self.start_topic} with RELIABLE QoS"
+ )
+
+ status_topic_qos = QoSProfile(
+ reliability=ReliabilityPolicy.RELIABLE,
+ history=HistoryPolicy.KEEP_LAST,
+ depth=10,
+ durability=DurabilityPolicy.VOLATILE,
+ )
+ self.status_publisher = self.create_publisher(
+ String, self.status_topic, status_topic_qos
+ )
+ self.get_logger().info(
+ f"Publishing to status topic: {self.status_topic} with RELIABLE QoS"
+ )
+
+ if self.pose_topic:
+ self.pose_subscriber = self.create_subscription(
+ PoseWithCovarianceStamped, self.pose_topic, self.pose_cov_callback, 10
+ )
+ self.get_logger().info(
+ f"Subscribing to pose topic (PoseWithCovarianceStamped from prm::base.pose.cov): {self.pose_topic}"
+ )
+ else:
+ self.pose_subscriber = None
+
+ if self.navigate_goal_topic:
+ self.navigate_goal_publisher = self.create_publisher(
+ PoseStamped, self.navigate_goal_topic, 10
+ )
+ self.get_logger().info(
+ f"Publishing to navigate goal: {self.navigate_goal_topic}"
+ )
+ else:
+ self.navigate_goal_publisher = None
+
+ if self.navigate_status_topic:
+ status_qos = QoSProfile(
+ reliability=ReliabilityPolicy.RELIABLE,
+ history=HistoryPolicy.KEEP_LAST,
+ depth=10,
+ )
+ self.navigate_status_subscriber = self.create_subscription(
+ Bool, self.navigate_status_topic, self.navigate_status_callback, status_qos
+ )
+ self.get_logger().info(
+ f"Subscribing to navigate status: {self.navigate_status_topic}"
+ )
+ else:
+ self.navigate_status_subscriber = None
+
+ if self.semantic_map_service_entry:
+ semantic_map_qos = QoSProfile(
+ reliability=ReliabilityPolicy.RELIABLE,
+ history=HistoryPolicy.KEEP_LAST,
+ depth=10,
+ durability=DurabilityPolicy.VOLATILE,
+ )
+ semantic_map_qos.deadline = Duration(seconds=0)
+ semantic_map_qos.lifespan = Duration(seconds=0)
+ semantic_map_qos.liveliness = LivelinessPolicy.AUTOMATIC
+ semantic_map_qos.liveliness_lease_duration = Duration(seconds=0)
+
+ self.semantic_map_client = self.create_client(
+ QuerySemanticMap,
+ self.semantic_map_service_entry,
+ qos_profile=semantic_map_qos,
+ )
+ self.get_logger().info(
+ f"Semantic map service client created: {self.semantic_map_service_entry}"
+ )
+ else:
+ self.semantic_map_client = None
+
+ self.current_skill_id = None
+ self.moving_in_progress = False
+ self._cancel_requested = False
+ self._target_replaced = False
+ self.target_object_id = None
+ self.stop_radius = 0.5
+ self.navigation_complete = False
+ self.latest_pose = None
+ self.status_timer = None
+ self.status_timer_running = False
+
+ self.get_logger().info("Move to object skill initialized")
+
+ def _query_services_and_primitives(self):
+ """Query robonix core for required services and primitives."""
+ max_retries = 5
+ retry_delay = 2.0
+
+ self.get_logger().info("Querying srv::semantic_map service...")
+ for attempt in range(max_retries):
+ try:
+ wait_timeout = 10.0 if attempt < 2 else 5.0
+ if not self.query_service_client.wait_for_service(
+ timeout_sec=wait_timeout
+ ):
+ self.get_logger().warn(
+ f" query_service not available (attempt {attempt + 1}/{max_retries})"
+ )
+ if attempt < max_retries - 1:
+ time.sleep(retry_delay)
+ continue
+ else:
+ self.get_logger().error(
+ " query_service not available after all retries"
+ )
+ break
+
+ request = QueryService.Request()
+ request.name = "srv::semantic_map"
+ request.filter = "{}"
+
+ future = self.query_service_client.call_async(request)
+ start_time = time.time()
+ timeout_sec = 3.0
+ while not future.done() and (time.time() - start_time) < timeout_sec:
+ rclpy.spin_once(self, timeout_sec=0.01)
+
+ if not future.done():
+ if attempt < max_retries - 1:
+ time.sleep(retry_delay)
+ continue
+ else:
+ break
+
+ response = future.result()
+ if response and response.instances:
+ instance = response.instances[0]
+ self.semantic_map_service_entry = instance.entry
+ self.get_logger().info(
+ f" Found semantic_map service: {self.semantic_map_service_entry}"
+ )
+ break
+ except Exception as e:
+ self.get_logger().error(
+ f"Error querying srv::semantic_map service: {e}"
+ )
+ if attempt < max_retries - 1:
+ time.sleep(retry_delay)
+ else:
+ break
+
+ self.get_logger().info("Querying prm::base.pose.cov...")
+ for attempt in range(max_retries):
+ try:
+ wait_timeout = 10.0 if attempt < 2 else 5.0
+ if not self.query_primitive_client.wait_for_service(
+ timeout_sec=wait_timeout
+ ):
+ if attempt < max_retries - 1:
+ time.sleep(retry_delay)
+ continue
+ else:
+ break
+
+ request = QueryPrimitive.Request()
+ request.name = "prm::base.pose.cov"
+ request.filter = "{}"
+
+ future = self.query_primitive_client.call_async(request)
+ start_time = time.time()
+ timeout_sec = 3.0
+ while not future.done() and (time.time() - start_time) < timeout_sec:
+ rclpy.spin_once(self, timeout_sec=0.01)
+
+ if not future.done():
+ if attempt < max_retries - 1:
+ time.sleep(retry_delay)
+ continue
+ else:
+ break
+
+ response = future.result()
+ if response and response.instances:
+ instance = response.instances[0]
+ output_schema = (
+ json.loads(instance.output_schema)
+ if isinstance(instance.output_schema, str)
+ else instance.output_schema
+ )
+ if "pose" in output_schema:
+ self.pose_topic = output_schema["pose"]
+ self.get_logger().info(
+ f" Found pose topic: {self.pose_topic} (from prm::base.pose.cov)"
+ )
+ break
+ except Exception as e:
+ self.get_logger().error(
+ f"Error querying prm::base.pose.cov: {e}")
+ if attempt < max_retries - 1:
+ time.sleep(retry_delay)
+ else:
+ break
+
+ self.get_logger().info("Querying prm::base.navigate...")
+ for attempt in range(max_retries):
+ try:
+ wait_timeout = 10.0 if attempt < 2 else 5.0
+ if not self.query_primitive_client.wait_for_service(
+ timeout_sec=wait_timeout
+ ):
+ if attempt < max_retries - 1:
+ time.sleep(retry_delay)
+ continue
+ else:
+ break
+
+ request = QueryPrimitive.Request()
+ request.name = "prm::base.navigate"
+ request.filter = "{}"
+
+ future = self.query_primitive_client.call_async(request)
+ start_time = time.time()
+ timeout_sec = 3.0
+ while not future.done() and (time.time() - start_time) < timeout_sec:
+ rclpy.spin_once(self, timeout_sec=0.01)
+
+ if not future.done():
+ if attempt < max_retries - 1:
+ time.sleep(retry_delay)
+ continue
+ else:
+ break
+
+ response = future.result()
+ if response and response.instances:
+ instance = response.instances[0]
+ input_schema = (
+ json.loads(instance.input_schema)
+ if isinstance(instance.input_schema, str)
+ else instance.input_schema
+ )
+ output_schema = (
+ json.loads(instance.output_schema)
+ if isinstance(instance.output_schema, str)
+ else instance.output_schema
+ )
+ if "goal" in input_schema:
+ self.navigate_goal_topic = input_schema["goal"]
+ self.get_logger().info(
+ f" Found navigate goal topic: {self.navigate_goal_topic}"
+ )
+ if "status" in output_schema:
+ self.navigate_status_topic = output_schema["status"]
+ self.get_logger().info(
+ f" Found navigate status topic: {self.navigate_status_topic}"
+ )
+ break
+ except Exception as e:
+ self.get_logger().error(
+ f"Error querying prm::base.navigate: {e}")
+ if attempt < max_retries - 1:
+ time.sleep(retry_delay)
+ else:
+ break
+
+ def start_callback(self, msg):
+ """Handle skill start request (input params or robonix terminate)."""
+ try:
+ data = json.loads(msg.data)
+ # Robonix can send terminate to start_topic to force-stop this skill
+ if data.get("terminate") and data.get("skill_id"):
+ sid = data.get("skill_id")
+ if self.moving_in_progress and sid == self.current_skill_id:
+ self.get_logger().info(
+ f"Received terminate for skill_id={sid}, stopping move_to_object"
+ )
+ self._cancel_requested = True
+ self.moving_in_progress = False
+ self._stop_status_timer()
+ self._publish_status(
+ sid,
+ "cancelled",
+ {"message": "Cancelled by robonix (terminate on start_topic)"},
+ errno=0,
+ )
+ return
+
+ skill_id = data.get("skill_id", "unknown")
+ params = data.get("params", {})
+ target_object_id = params.get("target_object_id", "")
+
+ if "stop_radius" in params:
+ self.stop_radius = float(params["stop_radius"])
+ elif "radius" in params:
+ self.stop_radius = float(params["radius"])
+ else:
+ self.stop_radius = 0.5
+
+ self.get_logger().info(
+ f"Received move_to_object request: skill_id={skill_id}, "
+ f"target_object_id={target_object_id}, stop_radius={self.stop_radius}m"
+ )
+
+ if self.moving_in_progress:
+ self.get_logger().info(
+ "New target received while moving, replacing with target_object_id=%s"
+ % target_object_id
+ )
+ self.current_skill_id = skill_id
+ self.target_object_id = target_object_id
+ self._cancel_requested = False
+ self._target_replaced = True
+ self._publish_status(
+ skill_id,
+ "running",
+ {
+ "message": "Target replaced, navigating to new object %s"
+ % target_object_id
+ },
+ errno=0,
+ )
+ return
+
+ if not target_object_id:
+ self.get_logger().error("target_object_id parameter is required!")
+ self._publish_status(
+ skill_id,
+ "error",
+ {"error": "target_object_id parameter is required"},
+ errno=2,
+ )
+ return
+
+ if not self.semantic_map_client:
+ self.get_logger().error("Semantic map service not available!")
+ self._publish_status(
+ skill_id,
+ "error",
+ {"error": "Semantic map service not available"},
+ errno=3,
+ )
+ return
+
+ if not self.navigate_goal_publisher:
+ self.get_logger().error("Navigation primitive not available!")
+ self._publish_status(
+ skill_id,
+ "error",
+ {"error": "Navigation primitive not available"},
+ errno=4,
+ )
+ return
+
+ self.current_skill_id = skill_id
+ self.target_object_id = target_object_id
+ self._cancel_requested = False
+ self.moving_in_progress = True
+
+ self._publish_status(
+ skill_id,
+ "running",
+ {
+ "message": f"Received request, searching for object with ID: {target_object_id}"
+ },
+ errno=0,
+ )
+
+ self._start_status_timer(skill_id)
+ self._start_move_operation()
+
+ except json.JSONDecodeError as e:
+ self.get_logger().error(f"Failed to parse start message JSON: {e}")
+ self._publish_status(
+ "unknown", "error", {"error": f"Invalid JSON: {e}"}, errno=5
+ )
+ except Exception as e:
+ self.get_logger().error(f"Error in start_callback: {e}")
+ import traceback
+
+ self.get_logger().error(f"Traceback:\n{traceback.format_exc()}")
+ self._publish_status("unknown", "error", {
+ "error": str(e)}, errno=6)
+
+ def _start_status_timer(self, skill_id):
+ """Start periodic status reporting (every 1 second)."""
+ import threading
+
+ self.status_timer_running = True
+
+ def status_timer_loop():
+ while self.status_timer_running and self.moving_in_progress:
+ time.sleep(1.0)
+ if self.status_timer_running and self.moving_in_progress:
+ if self.navigation_complete:
+ break
+ else:
+ current_message = (
+ f"Processing: searching for object {self.target_object_id}"
+ )
+ if self.latest_pose:
+ current_message = f"Processing: navigating to object {self.target_object_id}"
+ self._publish_status(
+ skill_id, "running", {"message": current_message}, errno=0
+ )
+
+ self.status_timer = threading.Thread(
+ target=status_timer_loop, daemon=True)
+ self.status_timer.start()
+
+ def _stop_status_timer(self):
+ """Stop periodic status reporting."""
+ self.status_timer_running = False
+
+ def _start_move_operation(self):
+ """Start the move operation in a separate thread."""
+ import threading
+
+ thread = threading.Thread(target=self._move_operation, daemon=True)
+ thread.start()
+
+ def _move_operation(self):
+ """Main move operation that finds and navigates to the target object."""
+ try:
+ while True:
+ self.get_logger().info(
+ f"Querying semantic map for object with ID: {self.target_object_id}"
+ )
+ try:
+ objects = self._query_semantic_map()
+ except TimeoutError as e:
+ error_msg = f"Semantic map query timeout: {str(e)}"
+ self.get_logger().error(error_msg)
+ self._stop_status_timer()
+ self._publish_status(
+ self.current_skill_id,
+ "error",
+ {"error": error_msg, "error_type": "timeout"},
+ errno=7,
+ )
+ self.moving_in_progress = False
+ return
+ except RuntimeError as e:
+ error_msg = f"Semantic map service error: {str(e)}"
+ self.get_logger().error(error_msg)
+ self._stop_status_timer()
+ self._publish_status(
+ self.current_skill_id,
+ "error",
+ {"error": error_msg, "error_type": "service_error"},
+ errno=7,
+ )
+ self.moving_in_progress = False
+ return
+
+ if not objects:
+ error_msg = "Semantic map query returned no objects (map may be empty)"
+ self.get_logger().error(error_msg)
+ self._stop_status_timer()
+ self._publish_status(
+ self.current_skill_id,
+ "error",
+ {"error": error_msg, "error_type": "empty_map"},
+ errno=7,
+ )
+ self.moving_in_progress = False
+ return
+
+ target_object = None
+ for obj in objects:
+ if obj.id == self.target_object_id:
+ target_object = obj
+ break
+
+ if not target_object:
+ self.get_logger().error(
+ f'Object with ID "{self.target_object_id}" not found in semantic map'
+ )
+ available_ids = [obj.id for obj in objects]
+ available_labels = [obj.label for obj in objects]
+ error_msg = (
+ f'Object with ID "{self.target_object_id}" not found in semantic map. '
+ f"Available object IDs: {available_ids[:10]}{'...' if len(available_ids) > 10 else ''}. "
+ f"This may happen if the object was removed or the ID from planning is incorrect."
+ )
+ self.get_logger().error(error_msg)
+ self._stop_status_timer()
+ self._publish_status(
+ self.current_skill_id,
+ "error",
+ {
+ "error": error_msg,
+ "requested_object_id": self.target_object_id,
+ "available_object_ids": available_ids,
+ "available_labels": available_labels,
+ },
+ errno=8,
+ )
+ self.moving_in_progress = False
+ return
+
+ self.get_logger().info(
+ f"Found target object: {target_object.id} ({target_object.label})"
+ )
+
+ object_pose = self._get_object_pose_in_map(target_object)
+ if not object_pose:
+ self.get_logger().error(
+ f"Could not get pose for object {target_object.id}"
+ )
+ self._stop_status_timer()
+ self._publish_status(
+ self.current_skill_id,
+ "error",
+ {"error": f"Could not get pose for object {target_object.id}"},
+ errno=9,
+ )
+ self.moving_in_progress = False
+ return
+
+ goal_pose = self._calculate_goal_pose_near_object(
+ object_pose, self.stop_radius
+ )
+
+ actual_distance = math.sqrt(
+ (goal_pose.pose.position.x - object_pose.pose.position.x) ** 2
+ + (goal_pose.pose.position.y - object_pose.pose.position.y) ** 2
+ )
+
+ self.get_logger().info(
+ f"Navigating to object {target_object.id} ({target_object.label}) - "
+ f"Goal at ({goal_pose.pose.position.x:.2f}, {goal_pose.pose.position.y:.2f}), "
+ f"will stop {actual_distance:.2f}m from object (requested: {self.stop_radius:.2f}m)"
+ )
+
+ self.navigation_complete = False
+ assert self.navigate_goal_publisher is not None, (
+ "Navigation goal publisher must be available"
+ )
+ self.navigate_goal_publisher.publish(goal_pose)
+
+ # Reach = at goal (xyz position only, no orientation) and stationary for a while
+ POSITION_DELTA_M = 0.35
+ STILL_DURATION_S = 1.0
+ STILL_MOVE_THRESHOLD_M = 0.05
+ time_first_at_goal = None
+ pose_first_at_goal = None
+ last_log_time = 0.0
+ LOG_INTERVAL_S = 2.0
+
+ self.get_logger().info(
+ "Waiting for pose-based completion: xyz dist<=%.2fm, still %.1fs"
+ % (POSITION_DELTA_M, STILL_DURATION_S)
+ )
+
+ timeout = 90.0
+ start_time = time.time()
+ while (
+ not self.navigation_complete
+ and (time.time() - start_time) < timeout
+ and not self._cancel_requested
+ and not self._target_replaced
+ ):
+ rclpy.spin_once(self, timeout_sec=0.1)
+ if self.navigation_complete or self._cancel_requested or self._target_replaced:
+ break
+ now = time.time()
+ if self.latest_pose is None:
+ if now - last_log_time >= LOG_INTERVAL_S:
+ self.get_logger().warn(
+ "No pose yet (amcl_pose?). elapsed=%.1fs" % (now - start_time)
+ )
+ last_log_time = now
+ continue
+ dist = math.sqrt(
+ (self.latest_pose.pose.position.x - goal_pose.pose.position.x) ** 2
+ + (self.latest_pose.pose.position.y - goal_pose.pose.position.y) ** 2
+ + (self.latest_pose.pose.position.z - goal_pose.pose.position.z) ** 2
+ )
+ at_goal = dist <= POSITION_DELTA_M
+ if at_goal:
+ if time_first_at_goal is None:
+ time_first_at_goal = time.time()
+ pose_first_at_goal = (
+ self.latest_pose.pose.position.x,
+ self.latest_pose.pose.position.y,
+ self.latest_pose.pose.position.z,
+ )
+ self.get_logger().info(
+ "At goal (xyz dist=%.3fm). Starting still timer (%.1fs)."
+ % (dist, STILL_DURATION_S)
+ )
+ elif (time.time() - time_first_at_goal) >= STILL_DURATION_S:
+ assert pose_first_at_goal is not None
+ dx = self.latest_pose.pose.position.x - pose_first_at_goal[0]
+ dy = self.latest_pose.pose.position.y - pose_first_at_goal[1]
+ dz = self.latest_pose.pose.position.z - pose_first_at_goal[2]
+ drift = math.sqrt(dx * dx + dy * dy + dz * dz)
+ if drift <= STILL_MOVE_THRESHOLD_M:
+ self.navigation_complete = True
+ self.get_logger().info(
+ "Navigation complete (at goal and stationary for %.1fs, drift=%.3fm)"
+ % (STILL_DURATION_S, drift)
+ )
+ elif now - last_log_time >= LOG_INTERVAL_S:
+ self.get_logger().info(
+ "At goal but moved too much: drift=%.3fm (max %.2fm). Resetting still timer."
+ % (drift, STILL_MOVE_THRESHOLD_M)
+ )
+ time_first_at_goal = None
+ pose_first_at_goal = None
+ last_log_time = now
+ elif now - last_log_time >= LOG_INTERVAL_S:
+ remaining = STILL_DURATION_S - (now - time_first_at_goal)
+ self.get_logger().info(
+ "At goal, waiting still: %.1fs left" % max(0, remaining)
+ )
+ last_log_time = now
+ else:
+ if time_first_at_goal is not None:
+ self.get_logger().info(
+ "Left goal (xyz dist=%.3fm). Resetting still timer."
+ % (dist,)
+ )
+ time_first_at_goal = None
+ pose_first_at_goal = None
+ if now - last_log_time >= LOG_INTERVAL_S:
+ self.get_logger().info(
+ "Approaching: xyz dist=%.3fm (need<=%.2f), pos=(%.2f,%.2f,%.2f)"
+ % (
+ dist,
+ POSITION_DELTA_M,
+ self.latest_pose.pose.position.x,
+ self.latest_pose.pose.position.y,
+ self.latest_pose.pose.position.z,
+ )
+ )
+ last_log_time = now
+
+ if self._target_replaced:
+ self._target_replaced = False
+ self.get_logger().info(
+ "Target replaced, switching to new object %s"
+ % self.target_object_id
+ )
+ continue
+
+ if self._cancel_requested:
+ self._stop_status_timer()
+ self._publish_status(
+ self.current_skill_id,
+ "cancelled",
+ {"message": "Cancelled by robonix (terminate on start_topic)"},
+ errno=0,
+ )
+ self.get_logger().info("Move to object cancelled by robonix")
+ self.moving_in_progress = False
+ return
+
+ if self.navigation_complete:
+ final_distance = None
+ if self.latest_pose:
+ final_distance = math.sqrt(
+ (self.latest_pose.pose.position.x -
+ object_pose.pose.position.x)
+ ** 2
+ + (
+ self.latest_pose.pose.position.y
+ - object_pose.pose.position.y
+ )
+ ** 2
+ )
+
+ result = {
+ "message": f"Successfully navigated to object: {target_object.label}",
+ "object_id": target_object.id,
+ "object_label": target_object.label,
+ "object_position": {
+ "x": object_pose.pose.position.x,
+ "y": object_pose.pose.position.y,
+ "z": object_pose.pose.position.z,
+ },
+ "goal_position": {
+ "x": goal_pose.pose.position.x,
+ "y": goal_pose.pose.position.y,
+ "z": goal_pose.pose.position.z,
+ },
+ "stop_radius": self.stop_radius,
+ }
+ if final_distance is not None:
+ result["final_distance_to_object"] = final_distance
+
+ self._stop_status_timer()
+ self._publish_status(self.current_skill_id,
+ "finished", result, errno=0)
+ self.get_logger().info(
+ f"Successfully navigated to object: {target_object.label} "
+ f"(stopped {final_distance:.2f}m away, requested: {self.stop_radius:.2f}m)"
+ if final_distance is not None
+ else f"Successfully navigated to object: {target_object.label}"
+ )
+ self.moving_in_progress = False
+ return
+ else:
+ self._stop_status_timer()
+ # Log why we timed out
+ if self.latest_pose is not None:
+ dist = math.sqrt(
+ (self.latest_pose.pose.position.x - goal_pose.pose.position.x) ** 2
+ + (self.latest_pose.pose.position.y - goal_pose.pose.position.y) ** 2
+ + (self.latest_pose.pose.position.z - goal_pose.pose.position.z) ** 2
+ )
+ self.get_logger().error(
+ "Navigation timeout. Last: xyz dist=%.3fm (need<=%.2f)"
+ % (dist, POSITION_DELTA_M)
+ )
+ else:
+ self.get_logger().error(
+ "Navigation timeout. No pose received (check amcl_pose topic)."
+ )
+ self._publish_status(
+ self.current_skill_id,
+ "error",
+ {"error": "Navigation timeout"},
+ errno=10,
+ )
+ self.moving_in_progress = False
+ return
+
+ except Exception as e:
+ self._stop_status_timer()
+ self.get_logger().error(f"Error in move operation: {e}")
+ import traceback
+
+ self.get_logger().error(f"Traceback:\n{traceback.format_exc()}")
+ self._publish_status(
+ self.current_skill_id, "error", {"error": str(e)}, errno=11
+ )
+ self.moving_in_progress = False
+
+ def _query_semantic_map(self):
+ """Query semantic map service for objects.
+
+ Returns:
+ list: List of objects from semantic map
+
+ Raises:
+ TimeoutError: If the query times out
+ RuntimeError: If the service is not available or other errors occur
+ """
+ if not self.semantic_map_client:
+ raise RuntimeError("Semantic map client not initialized")
+
+ # Wait for service
+ if not self.semantic_map_client.wait_for_service(timeout_sec=5.0):
+ raise RuntimeError("Semantic map service not available")
+
+ request = QuerySemanticMap.Request()
+ request.types = []
+
+ future = self.semantic_map_client.call_async(request)
+
+ start_time = time.time()
+ timeout = 10.0
+ while not future.done() and (time.time() - start_time) < timeout:
+ rclpy.spin_once(self, timeout_sec=0.1)
+
+ if not future.done():
+ error_msg = f"Semantic map query timeout after {timeout}s"
+ self.get_logger().error(error_msg)
+ raise TimeoutError(error_msg)
+
+ try:
+ response = future.result()
+ assert response is not None, "Semantic map query response must not be None"
+ objects = list(response.objects)
+ self.get_logger().info(
+ f"Query semantic map returned {len(objects)} objects"
+ )
+ if objects:
+ object_ids = [obj.id for obj in objects]
+ object_labels = [obj.label for obj in objects]
+ self.get_logger().debug(
+ f"Available objects: {list(zip(object_ids[:10], object_labels[:10]))}{'...' if len(object_ids) > 10 else ''}"
+ )
+ return objects
+ except Exception as e:
+ error_msg = f"Error querying semantic map: {e}"
+ self.get_logger().error(error_msg)
+ import traceback
+
+ self.get_logger().error(f"Traceback:\n{traceback.format_exc()}")
+ raise RuntimeError(error_msg) from e
+
+ def _get_object_pose_in_map(self, obj):
+ """Get object pose in map frame."""
+ for frame_mapping in obj.frame_mapping:
+ if frame_mapping.frame_id == "map":
+ pose = PoseStamped()
+ pose.header.frame_id = "map"
+ pose.header.stamp = self.get_clock().now().to_msg()
+ pose.pose.position.x = float(frame_mapping.center.x)
+ pose.pose.position.y = float(frame_mapping.center.y)
+ pose.pose.position.z = float(frame_mapping.center.z)
+ pose.pose.orientation.w = 1.0
+ return pose
+ return None
+
+ def _yaw_from_quat(self, q):
+ """Extract yaw (radians) from geometry_msgs Quaternion (x, y, z, w)."""
+ return math.atan2(
+ 2.0 * (q.w * q.z + q.x * q.y),
+ 1.0 - 2.0 * (q.y * q.y + q.z * q.z),
+ )
+
+ def _calculate_goal_pose_near_object(self, object_pose, stop_radius):
+ """
+ Calculate goal pose near object, stopping at specified radius to avoid collision.
+
+ Args:
+ object_pose: PoseStamped of the object in map frame
+ stop_radius: Distance in meters to stop before reaching the object
+
+ Returns:
+ PoseStamped: Goal pose that is stop_radius meters away from the object
+ """
+ goal_pose = PoseStamped()
+ goal_pose.header = object_pose.header
+ goal_pose.header.stamp = self.get_clock().now().to_msg()
+
+ object_x = object_pose.pose.position.x
+ object_y = object_pose.pose.position.y
+
+ if self.latest_pose:
+ robot_x = self.latest_pose.pose.position.x
+ robot_y = self.latest_pose.pose.position.y
+
+ dx = object_x - robot_x
+ dy = object_y - robot_y
+ distance_to_object = math.sqrt(dx * dx + dy * dy)
+
+ if distance_to_object > stop_radius:
+ unit_dx = dx / distance_to_object
+ unit_dy = dy / distance_to_object
+ goal_pose.pose.position.x = object_x - unit_dx * stop_radius
+ goal_pose.pose.position.y = object_y - unit_dy * stop_radius
+ else:
+ if distance_to_object > 0.1:
+ goal_pose.pose.position.x = robot_x
+ goal_pose.pose.position.y = robot_y
+ else:
+ goal_pose.pose.position.x = object_x - stop_radius
+ goal_pose.pose.position.y = object_y
+ else:
+ goal_pose.pose.position.x = object_x - stop_radius
+ goal_pose.pose.position.y = object_y
+
+ goal_pose.pose.position.z = object_pose.pose.position.z
+
+ dx_to_object = object_x - goal_pose.pose.position.x
+ dy_to_object = object_y - goal_pose.pose.position.y
+ yaw = math.atan2(dy_to_object, dx_to_object)
+
+ goal_pose.pose.orientation.z = math.sin(yaw / 2.0)
+ goal_pose.pose.orientation.w = math.cos(yaw / 2.0)
+
+ return goal_pose
+
+ def pose_cov_callback(self, msg):
+ """Handle PoseWithCovarianceStamped updates and convert to PoseStamped."""
+ pose_stamped = PoseStamped()
+ pose_stamped.header = msg.header
+ pose_stamped.pose = msg.pose.pose
+ self.latest_pose = pose_stamped
+
+ def navigate_status_callback(self, msg):
+ """Handle navigation status updates."""
+ if hasattr(msg, "data"):
+ success = msg.data
+ if success:
+ self.navigation_complete = True
+ self.get_logger().info("Navigation completed successfully")
+
+ def _publish_status(self, skill_id, state, result, errno=0):
+ """
+ Publish skill status to status_topic.
+
+ Standard status format (matching executor expectations):
+ {
+ "skill_id": string, # Skill execution ID
+ "state": string, # "running" | "finished" | "error"
+ "result": any, # Result data (any JSON-serializable value)
+ "errno": int, # Error number (0 = success, non-zero = error)
+ "error": string, # Optional: Error message (extracted from result if present)
+ "message": string, # Optional: Status message (extracted from result if present)
+ "error_message": string # Optional: Alternative error message field
+ }
+
+ The executor extracts error information from these fields in order:
+ 1. "error"
+ 2. "message"
+ 3. "error_message"
+ 4. If none found and errno != 0, generates: "Skill execution failed with errno={errno}"
+ """
+ status_msg = {
+ "skill_id": skill_id,
+ "state": state,
+ "result": result,
+ "errno": errno,
+ }
+
+ if isinstance(result, dict):
+ if "error" in result:
+ status_msg["error"] = result["error"]
+ if "message" in result:
+ status_msg["message"] = result["message"]
+ if "error_message" in result:
+ status_msg["error_message"] = result["error_message"]
+
+ if (state == "error" or errno != 0) and "error" not in status_msg:
+ if isinstance(result, dict) and "error" in result:
+ status_msg["error"] = result["error"]
+ else:
+ status_msg["error"] = (
+ f"Skill execution failed: state={state}, errno={errno}"
+ )
+
+ msg = String()
+ msg.data = json.dumps(status_msg)
+ self.status_publisher.publish(msg)
+ self.get_logger().info(
+ f"Published status: skill_id={skill_id}, state={state}, errno={errno}"
+ )
+
+
+def main(args=None):
+ rclpy.init(args=args)
+ move_to_object_skill = MoveToObjectSkill()
+
+ shutdown_requested = False
+
+ def signal_handler(signum, frame):
+ nonlocal shutdown_requested
+ shutdown_requested = True
+ move_to_object_skill.get_logger().info(
+ f"Received signal {signum}, shutting down..."
+ )
+ rclpy.shutdown()
+
+ signal.signal(signal.SIGTERM, signal_handler)
+ signal.signal(signal.SIGINT, signal_handler)
+
+ try:
+ rclpy.spin(move_to_object_skill)
+ except KeyboardInterrupt:
+ shutdown_requested = True
+ move_to_object_skill.get_logger().info(
+ "Received KeyboardInterrupt, shutting down..."
+ )
+ finally:
+ move_to_object_skill.destroy_node()
+ rclpy.shutdown()
+ if shutdown_requested:
+ move_to_object_skill.get_logger().info(
+ "Move to object skill shutdown complete"
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/rust/provider/navigation_skills/navigation_skills_provider/wandering_skill.py b/rust/provider/navigation_skills/navigation_skills_provider/wandering_skill.py
new file mode 100755
index 0000000..12326f0
--- /dev/null
+++ b/rust/provider/navigation_skills/navigation_skills_provider/wandering_skill.py
@@ -0,0 +1,564 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: MulanPSL-2.0
+# Wandering Skill
+#
+# Random wandering skill that explores the environment by navigating to random positions.
+# The skill generates random goal poses and navigates to them repeatedly.
+""""""
+
+import rclpy
+from rclpy.node import Node
+from rclpy.qos import (
+ QoSProfile,
+ ReliabilityPolicy,
+ HistoryPolicy,
+ DurabilityPolicy,
+ LivelinessPolicy,
+)
+from rclpy.duration import Duration
+from std_msgs.msg import String, Bool
+from geometry_msgs.msg import PoseStamped, PoseWithCovarianceStamped
+import json
+import random
+import time
+import signal
+import math
+import sys
+from robonix_sdk.srv import QueryPrimitive
+
+
+class WanderingSkill(Node):
+ """Implements wandering skill that randomly explores the environment."""
+
+ def __init__(self):
+ super().__init__("wandering_skill")
+
+ self.start_topic = "/robot1/skill/wandering/start"
+ self.status_topic = "/robot1/skill/wandering/status"
+
+ self.pose_topic = None
+ self.navigate_goal_topic = None
+ self.navigate_status_topic = None
+
+ service_qos = QoSProfile(
+ reliability=ReliabilityPolicy.RELIABLE,
+ history=HistoryPolicy.KEEP_LAST,
+ depth=10,
+ durability=DurabilityPolicy.VOLATILE,
+ )
+ service_qos.deadline = Duration(seconds=0)
+ service_qos.lifespan = Duration(seconds=0)
+ service_qos.liveliness = LivelinessPolicy.AUTOMATIC
+ service_qos.liveliness_lease_duration = Duration(seconds=0)
+
+ self.query_primitive_client = self.create_client(
+ QueryPrimitive, "/rbnx/prm/query", qos_profile=service_qos
+ )
+ self.get_logger().info("QueryPrimitive service client created")
+
+ self._query_primitives()
+
+ self.start_subscriber = self.create_subscription(
+ String, self.start_topic, self.start_callback, 10
+ )
+ self.get_logger().info(
+ f"Subscribing to start topic: {self.start_topic}")
+
+ self.status_publisher = self.create_publisher(
+ String, self.status_topic, 10)
+ self.get_logger().info(
+ f"Publishing to status topic: {self.status_topic}")
+
+ if self.pose_topic:
+ self.pose_subscriber = self.create_subscription(
+ PoseWithCovarianceStamped, self.pose_topic, self.pose_cov_callback, 10
+ )
+ self.get_logger().info(
+ f"Subscribing to pose topic (PoseWithCovarianceStamped from prm::base.pose.cov): {self.pose_topic}"
+ )
+ else:
+ self.pose_subscriber = None
+
+ if self.navigate_goal_topic:
+ self.navigate_goal_publisher = self.create_publisher(
+ PoseStamped, self.navigate_goal_topic, 10
+ )
+ self.get_logger().info(
+ f"Publishing to navigate goal: {self.navigate_goal_topic}"
+ )
+ else:
+ self.navigate_goal_publisher = None
+
+ if self.navigate_status_topic:
+ self.navigate_status_subscriber = self.create_subscription(
+ Bool, self.navigate_status_topic, self.navigate_status_callback, 10
+ )
+ self.get_logger().info(
+ f"Subscribing to navigate status: {self.navigate_status_topic}"
+ )
+ else:
+ self.navigate_status_subscriber = None
+
+ self.current_skill_id = None
+ self.wandering_in_progress = False
+ self._cancel_requested = False
+ self.navigation_complete = False
+ self.latest_pose = None
+ self.wander_radius = 5.0
+ self.starting_position = None
+
+ self.get_logger().info("Wandering skill initialized")
+
+ def _query_primitives(self):
+ """Query robonix core for required primitives. Exits if any required primitive is not found."""
+ max_retries = 5
+ retry_delay = 2.0
+
+ self.get_logger().info("Querying prm::base.pose.cov...")
+ pose_found = False
+ for attempt in range(max_retries):
+ try:
+ wait_timeout = 10.0 if attempt < 2 else 5.0
+ if not self.query_primitive_client.wait_for_service(
+ timeout_sec=wait_timeout
+ ):
+ if attempt < max_retries - 1:
+ time.sleep(retry_delay)
+ continue
+ else:
+ break
+
+ request = QueryPrimitive.Request()
+ request.name = "prm::base.pose.cov"
+ request.filter = "{}"
+
+ future = self.query_primitive_client.call_async(request)
+ start_time = time.time()
+ timeout_sec = 3.0
+ while not future.done() and (time.time() - start_time) < timeout_sec:
+ rclpy.spin_once(self, timeout_sec=0.01)
+
+ if not future.done():
+ if attempt < max_retries - 1:
+ time.sleep(retry_delay)
+ continue
+ else:
+ break
+
+ response = future.result()
+ if response and response.instances:
+ instance = response.instances[0]
+ output_schema = (
+ json.loads(instance.output_schema)
+ if isinstance(instance.output_schema, str)
+ else instance.output_schema
+ )
+ if "pose" in output_schema:
+ self.pose_topic = output_schema["pose"]
+ self.get_logger().info(
+ f" Found pose topic: {self.pose_topic} (from prm::base.pose.cov)"
+ )
+ pose_found = True
+ break
+ except Exception as e:
+ self.get_logger().error(
+ f"Error querying prm::base.pose.cov: {e}")
+ if attempt < max_retries - 1:
+ time.sleep(retry_delay)
+ else:
+ break
+
+ if not pose_found:
+ self.get_logger().error(
+ "Failed to query prm::base.pose.cov after all retries. Exiting."
+ )
+ sys.exit(1)
+
+ self.get_logger().info("Querying prm::base.navigate...")
+ navigate_found = False
+ for attempt in range(max_retries):
+ try:
+ wait_timeout = 10.0 if attempt < 2 else 5.0
+ if not self.query_primitive_client.wait_for_service(
+ timeout_sec=wait_timeout
+ ):
+ if attempt < max_retries - 1:
+ time.sleep(retry_delay)
+ continue
+ else:
+ break
+
+ request = QueryPrimitive.Request()
+ request.name = "prm::base.navigate"
+ request.filter = "{}"
+
+ future = self.query_primitive_client.call_async(request)
+ start_time = time.time()
+ timeout_sec = 3.0
+ while not future.done() and (time.time() - start_time) < timeout_sec:
+ rclpy.spin_once(self, timeout_sec=0.01)
+
+ if not future.done():
+ if attempt < max_retries - 1:
+ time.sleep(retry_delay)
+ continue
+ else:
+ break
+
+ response = future.result()
+ if response and response.instances:
+ instance = response.instances[0]
+ input_schema = (
+ json.loads(instance.input_schema)
+ if isinstance(instance.input_schema, str)
+ else instance.input_schema
+ )
+ output_schema = (
+ json.loads(instance.output_schema)
+ if isinstance(instance.output_schema, str)
+ else instance.output_schema
+ )
+ if "goal" in input_schema:
+ self.navigate_goal_topic = input_schema["goal"]
+ self.get_logger().info(
+ f" Found navigate goal topic: {self.navigate_goal_topic}"
+ )
+ navigate_found = True
+ if "status" in output_schema:
+ self.navigate_status_topic = output_schema["status"]
+ self.get_logger().info(
+ f" Found navigate status topic: {self.navigate_status_topic}"
+ )
+ if navigate_found:
+ break
+ except Exception as e:
+ self.get_logger().error(
+ f"Error querying prm::base.navigate: {e}")
+ if attempt < max_retries - 1:
+ time.sleep(retry_delay)
+ else:
+ break
+
+ if not navigate_found:
+ self.get_logger().error(
+ "Failed to query prm::base.navigate after all retries. Exiting."
+ )
+ sys.exit(1)
+
+ def start_callback(self, msg):
+ """Handle skill start request (input params or robonix terminate)."""
+ try:
+ data = json.loads(msg.data)
+ # Robonix can send terminate to start_topic to force-stop this skill
+ if data.get("terminate") and data.get("skill_id"):
+ sid = data.get("skill_id")
+ if self.wandering_in_progress and sid == self.current_skill_id:
+ self.get_logger().info(
+ f"Received terminate for skill_id={sid}, stopping wandering"
+ )
+ self._cancel_requested = True
+ self.wandering_in_progress = False
+ self._publish_status(
+ sid,
+ "cancelled",
+ {"message": "Cancelled by robonix (terminate on start_topic)"},
+ errno=0,
+ )
+ return
+
+ skill_id = data.get("skill_id", "unknown")
+ params = data.get("params", {})
+
+ if "wander_radius" in params:
+ self.wander_radius = float(params["wander_radius"])
+
+ self.get_logger().info(
+ f"Received wandering request: skill_id={skill_id}, wander_radius={self.wander_radius}"
+ )
+
+ if self.wandering_in_progress:
+ self.get_logger().warn(
+ "Wandering already in progress, ignoring request"
+ )
+ self._publish_status(
+ skill_id,
+ "error",
+ {"error": "Operation already in progress"},
+ errno=1,
+ )
+ return
+
+ if not self.navigate_goal_publisher:
+ self.get_logger().error("Navigation primitive not available!")
+ self._publish_status(
+ skill_id,
+ "error",
+ {"error": "Navigation primitive not available"},
+ errno=2,
+ )
+ return
+
+ if not self.latest_pose:
+ self.get_logger().warn("No initial pose available, waiting...")
+ for _ in range(10):
+ rclpy.spin_once(self, timeout_sec=0.5)
+ if self.latest_pose:
+ break
+
+ if not self.latest_pose:
+ self.get_logger().error("Could not get initial pose!")
+ self._publish_status(
+ skill_id,
+ "error",
+ {"error": "Could not get initial pose"},
+ errno=5,
+ )
+ return
+
+ self.starting_position = (
+ self.latest_pose.pose.position.x,
+ self.latest_pose.pose.position.y,
+ )
+
+ self.current_skill_id = skill_id
+ self._cancel_requested = False
+ self.wandering_in_progress = True
+
+ self._publish_status(
+ skill_id, "running", {"message": "Starting wandering..."}
+ )
+
+ self._start_wandering_loop()
+
+ except json.JSONDecodeError as e:
+ self.get_logger().error(f"Failed to parse start message JSON: {e}")
+ self._publish_status(
+ "unknown", "error", {"error": f"Invalid JSON: {e}"}, errno=3
+ )
+ except Exception as e:
+ self.get_logger().error(f"Error in start_callback: {e}")
+ import traceback
+
+ self.get_logger().error(f"Traceback:\n{traceback.format_exc()}")
+ self._publish_status("unknown", "error", {
+ "error": str(e)}, errno=4)
+
+ def _start_wandering_loop(self):
+ """Start the wandering loop."""
+ import threading
+
+ thread = threading.Thread(target=self._wandering_loop, daemon=True)
+ thread.start()
+
+ def _wandering_loop(self):
+ """Main wandering loop that generates random goals and navigates to them."""
+ max_iterations = 20
+ iteration = 0
+
+ while (
+ self.wandering_in_progress
+ and not self._cancel_requested
+ and iteration < max_iterations
+ ):
+ iteration += 1
+ self.get_logger().info(
+ f"Wandering iteration {iteration}/{max_iterations}")
+
+ try:
+ if not self.latest_pose:
+ self.get_logger().warn("No current pose available, waiting...")
+ time.sleep(1.0)
+ continue
+
+ current_x = self.latest_pose.pose.position.x
+ current_y = self.latest_pose.pose.position.y
+
+ if self.starting_position:
+ angle = random.uniform(0, 2 * math.pi)
+ distance = random.uniform(1.0, self.wander_radius)
+ goal_x = self.starting_position[0] + \
+ distance * math.cos(angle)
+ goal_y = self.starting_position[1] + \
+ distance * math.sin(angle)
+ else:
+ angle = random.uniform(0, 2 * math.pi)
+ distance = random.uniform(1.0, 3.0)
+ goal_x = current_x + distance * math.cos(angle)
+ goal_y = current_y + distance * math.sin(angle)
+
+ goal_yaw = random.uniform(-math.pi, math.pi)
+
+ goal_pose = PoseStamped()
+ goal_pose.header.frame_id = "map"
+ goal_pose.header.stamp = self.get_clock().now().to_msg()
+ goal_pose.pose.position.x = goal_x
+ goal_pose.pose.position.y = goal_y
+ goal_pose.pose.position.z = 0.0
+
+ goal_pose.pose.orientation.z = math.sin(goal_yaw / 2.0)
+ goal_pose.pose.orientation.w = math.cos(goal_yaw / 2.0)
+
+ self.get_logger().info(
+ f"Navigating to random goal at ({goal_x:.2f}, {goal_y:.2f}), "
+ f"yaw={math.degrees(goal_yaw):.1f}°"
+ )
+
+ self.navigation_complete = False
+ if self.navigate_goal_publisher is None:
+ raise RuntimeError(
+ "Navigation goal publisher is not available")
+ self.navigate_goal_publisher.publish(goal_pose)
+
+ timeout = 90.0
+ start_time = time.time()
+ while (
+ not self.navigation_complete
+ and (time.time() - start_time) < timeout
+ and not self._cancel_requested
+ ):
+ rclpy.spin_once(self, timeout_sec=0.1)
+
+ if self.navigation_complete:
+ self.get_logger().info("Navigation completed successfully")
+ self._publish_status(
+ self.current_skill_id,
+ "running",
+ {
+ "message": f"Reached goal {iteration}",
+ "iteration": iteration,
+ "goal_position": {"x": goal_x, "y": goal_y},
+ },
+ errno=0,
+ )
+ else:
+ self.get_logger().warn(
+ f"Navigation timeout for iteration {iteration}"
+ )
+
+ time.sleep(1.0)
+
+ except Exception as e:
+ self.get_logger().error(f"Error in wandering loop: {e}")
+ import traceback
+
+ self.get_logger().error(
+ f"Traceback:\n{traceback.format_exc()}")
+ time.sleep(2.0)
+
+ self.wandering_in_progress = False
+ if self._cancel_requested:
+ self._publish_status(
+ self.current_skill_id,
+ "cancelled",
+ {"message": "Cancelled by robonix (terminate on start_topic)"},
+ errno=0,
+ )
+ self.get_logger().info("Wandering cancelled by robonix")
+ else:
+ result = {"message": "Wandering completed", "iterations": iteration}
+ self._publish_status(
+ self.current_skill_id, "finished", result, errno=0
+ )
+ self.get_logger().info(
+ f"Wandering completed after {iteration} iterations."
+ )
+
+ def pose_cov_callback(self, msg):
+ """Handle PoseWithCovarianceStamped updates and convert to PoseStamped."""
+ pose_stamped = PoseStamped()
+ pose_stamped.header = msg.header
+ pose_stamped.pose = msg.pose.pose
+ self.latest_pose = pose_stamped
+
+ def navigate_status_callback(self, msg):
+ """Handle navigation status updates."""
+ if hasattr(msg, "data"):
+ success = msg.data
+ if success:
+ self.navigation_complete = True
+ self.get_logger().info("Navigation completed successfully")
+
+ def _publish_status(self, skill_id, state, result, errno=0):
+ """
+ Publish skill status to status_topic.
+
+ Standard status format (matching executor expectations):
+ {
+ "skill_id": string, # Skill execution ID
+ "state": string, # "running" | "finished" | "error"
+ "result": any, # Result data (any JSON-serializable value)
+ "errno": int, # Error number (0 = success, non-zero = error)
+ "error": string, # Optional: Error message (extracted from result if present)
+ "message": string, # Optional: Status message (extracted from result if present)
+ "error_message": string # Optional: Alternative error message field
+ }
+
+ The executor extracts error information from these fields in order:
+ 1. "error"
+ 2. "message"
+ 3. "error_message"
+ 4. If none found and errno != 0, generates: "Skill execution failed with errno={errno}"
+ """
+ status_msg = {
+ "skill_id": skill_id,
+ "state": state,
+ "result": result,
+ "errno": errno,
+ }
+
+ if isinstance(result, dict):
+ if "error" in result:
+ status_msg["error"] = result["error"]
+ if "message" in result:
+ status_msg["message"] = result["message"]
+ if "error_message" in result:
+ status_msg["error_message"] = result["error_message"]
+
+ if (state == "error" or errno != 0) and "error" not in status_msg:
+ if isinstance(result, dict) and "error" in result:
+ status_msg["error"] = result["error"]
+ else:
+ status_msg["error"] = (
+ f"Skill execution failed: state={state}, errno={errno}"
+ )
+
+ msg = String()
+ msg.data = json.dumps(status_msg)
+ self.status_publisher.publish(msg)
+ self.get_logger().info(
+ f"Published status: skill_id={skill_id}, state={state}, errno={errno}"
+ )
+
+
+def main(args=None):
+ rclpy.init(args=args)
+ wandering_skill = WanderingSkill()
+
+ shutdown_requested = False
+
+ def signal_handler(signum, frame):
+ nonlocal shutdown_requested
+ shutdown_requested = True
+ wandering_skill.get_logger().info(
+ f"Received signal {signum}, shutting down...")
+ rclpy.shutdown()
+
+ signal.signal(signal.SIGTERM, signal_handler)
+ signal.signal(signal.SIGINT, signal_handler)
+
+ try:
+ rclpy.spin(wandering_skill)
+ except KeyboardInterrupt:
+ shutdown_requested = True
+ wandering_skill.get_logger().info(
+ "Received KeyboardInterrupt, shutting down..."
+ )
+ finally:
+ wandering_skill.destroy_node()
+ rclpy.shutdown()
+ if shutdown_requested:
+ wandering_skill.get_logger().info("Wandering skill shutdown complete")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/rust/provider/demo_package/package.xml b/rust/provider/navigation_skills/package.xml
similarity index 69%
rename from rust/provider/demo_package/package.xml
rename to rust/provider/navigation_skills/package.xml
index 1349d68..61fcf6d 100644
--- a/rust/provider/demo_package/package.xml
+++ b/rust/provider/navigation_skills/package.xml
@@ -1,15 +1,18 @@
- demo_rgb_provider
+ navigation_skills_provider
0.0.1
- Demo RGB camera package that outputs random color images
+ Navigation skills package providing wandering and move_to_object skills
root
MulanPSL-2.0
+ ament_python
+
rclpy
- sensor_msgs
std_msgs
+ geometry_msgs
+ robonix_sdk
ament_copyright
ament_flake8
@@ -20,4 +23,3 @@
ament_python
-
diff --git a/rust/provider/navigation_skills/rbnx/build.sh b/rust/provider/navigation_skills/rbnx/build.sh
new file mode 100755
index 0000000..a1a2ce8
--- /dev/null
+++ b/rust/provider/navigation_skills/rbnx/build.sh
@@ -0,0 +1,67 @@
+#!/bin/bash
+# SPDX-License-Identifier: MulanPSL-2.0
+# Build Navigation Skills Package Script
+#
+# Build script for navigation_skills_provider package
+
+set -e # Exit on error
+
+echo "Building navigation_skills_provider package..."
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PACKAGE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
+cd "$PACKAGE_DIR"
+
+# Source ROS2 setup if available
+if [ -f /opt/ros/humble/setup.bash ]; then
+ source /opt/ros/humble/setup.bash
+else
+ echo "Error: ROS2 Humble setup.bash not found at /opt/ros/humble/setup.bash"
+ exit 1
+fi
+
+# Find robonix-sdk directory (for robonix_sdk ROS2 messages)
+ROBONIX_SDK_DIR=""
+if [ -n "$ROBONIX_SDK_PATH" ] && [ -d "$ROBONIX_SDK_PATH" ]; then
+ ROBONIX_SDK_DIR="$ROBONIX_SDK_PATH"
+else
+ # Search upward from package directory for robonix-sdk
+ SEARCH_DIR="$PACKAGE_DIR"
+ while [ "$SEARCH_DIR" != "/" ]; do
+ if [ -d "$SEARCH_DIR/robonix-sdk" ]; then
+ ROBONIX_SDK_DIR="$SEARCH_DIR/robonix-sdk"
+ break
+ fi
+ SEARCH_DIR="$(dirname "$SEARCH_DIR")"
+ done
+fi
+
+if [ -n "$ROBONIX_SDK_DIR" ] && [ -f "$ROBONIX_SDK_DIR/install/setup.bash" ]; then
+ echo "Found robonix-sdk at: $ROBONIX_SDK_DIR"
+ # Source robonix-sdk setup to ensure robonix_sdk messages are available
+ source "$ROBONIX_SDK_DIR/install/setup.bash" 2>/dev/null || true
+else
+ echo "Warning: robonix-sdk not found, robonix_sdk messages may not be available at build time"
+ echo "Note: robonix_sdk will be available at runtime if robonix-sdk setup.bash is sourced"
+fi
+
+# Clean previous build if it exists
+if [ -d "build" ] || [ -d "install" ]; then
+ echo "Cleaning previous build artifacts..."
+ rm -rf build install log
+fi
+
+# Build package using colcon
+echo "Building navigation_skills_provider with colcon..."
+if ! command -v colcon > /dev/null 2>&1; then
+ echo "Error: colcon not found. Please install colcon-common-extensions."
+ exit 1
+fi
+
+# Build with proper environment
+colcon build \
+ --symlink-install \
+ --packages-select navigation_skills_provider
+
+echo "Package built successfully!"
+echo "Build completed successfully!"
diff --git a/rust/provider/navigation_skills/rbnx/start_move_to_object.sh b/rust/provider/navigation_skills/rbnx/start_move_to_object.sh
new file mode 100755
index 0000000..d32cee6
--- /dev/null
+++ b/rust/provider/navigation_skills/rbnx/start_move_to_object.sh
@@ -0,0 +1,44 @@
+#!/bin/bash
+# SPDX-License-Identifier: MulanPSL-2.0
+set -e
+_log() { echo "[rbnx] $*" >&2; }
+_log "start_move_to_object.sh: starting skl::move_to_object"
+
+[ -z "$ROBONIX_SDK_PATH" ] && [ -f "${HOME:-/tmp}/.robonix/config.yaml" ] && \
+ ROBONIX_SDK_PATH=$(grep 'robonix_sdk_path' "${HOME:-/tmp}/.robonix/config.yaml" 2>/dev/null | sed 's/.*:[[:space:]]*//;s/[[:space:]]*$//' | tr -d "\"'")
+if [ -n "$ROBONIX_SDK_PATH" ]; then
+ _log "ROBONIX_SDK_PATH=${ROBONIX_SDK_PATH}"
+ [ -f "$ROBONIX_SDK_PATH/install/setup.bash" ] && source "$ROBONIX_SDK_PATH/install/setup.bash" && _log "sourced SDK setup.bash" || _log "SDK setup.bash not found, skip"
+else
+ _log "ROBONIX_SDK_PATH not set (no config or empty)"
+fi
+if [ -f /opt/ros/humble/setup.bash ]; then
+ source /opt/ros/humble/setup.bash && _log "sourced ROS2 humble"
+else
+ _log "ROS2 /opt/ros/humble/setup.bash not found"
+fi
+cd "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+_log "PWD=${PWD}"
+
+# Prefer source layout when skill module exists (avoids ros2 run when no colcon install)
+if [ -f navigation_skills_provider/move_to_object_skill.py ]; then
+ _log "layout=source, using python3 -m navigation_skills_provider.move_to_object_skill"
+ export PYTHONPATH="${PWD}:${PYTHONPATH}"
+ if [ -n "$ROBONIX_SDK_PATH" ]; then
+ SDK_PY=$(find "$ROBONIX_SDK_PATH/install" -type d -path "*/dist-packages" 2>/dev/null | head -1)
+ if [ -n "$SDK_PY" ]; then
+ export PYTHONPATH="${SDK_PY}:${PYTHONPATH}"
+ _log "PYTHONPATH includes SDK dist-packages: ${SDK_PY}"
+ else
+ _log "SDK dist-packages not found under ROBONIX_SDK_PATH/install, robonix_sdk may fail to import"
+ fi
+ fi
+ _log "exec: python3 -m navigation_skills_provider.move_to_object_skill"
+ exec python3 -m navigation_skills_provider.move_to_object_skill
+fi
+if [ -f install/setup.bash ]; then
+ _log "layout=install, using ros2 run navigation_skills_provider move_to_object_skill"
+ source install/setup.bash && exec ros2 run navigation_skills_provider move_to_object_skill
+fi
+_log "error: no navigation_skills_provider/move_to_object_skill.py and no install/setup.bash"
+exit 1
diff --git a/rust/provider/navigation_skills/rbnx/start_wandering.sh b/rust/provider/navigation_skills/rbnx/start_wandering.sh
new file mode 100755
index 0000000..12ec864
--- /dev/null
+++ b/rust/provider/navigation_skills/rbnx/start_wandering.sh
@@ -0,0 +1,44 @@
+#!/bin/bash
+# SPDX-License-Identifier: MulanPSL-2.0
+set -e
+_log() { echo "[rbnx] $*" >&2; }
+_log "start_wandering.sh: starting skl::wandering"
+
+[ -z "$ROBONIX_SDK_PATH" ] && [ -f "${HOME:-/tmp}/.robonix/config.yaml" ] && \
+ ROBONIX_SDK_PATH=$(grep 'robonix_sdk_path' "${HOME:-/tmp}/.robonix/config.yaml" 2>/dev/null | sed 's/.*:[[:space:]]*//;s/[[:space:]]*$//' | tr -d "\"'")
+if [ -n "$ROBONIX_SDK_PATH" ]; then
+ _log "ROBONIX_SDK_PATH=${ROBONIX_SDK_PATH}"
+ [ -f "$ROBONIX_SDK_PATH/install/setup.bash" ] && source "$ROBONIX_SDK_PATH/install/setup.bash" && _log "sourced SDK setup.bash" || _log "SDK setup.bash not found, skip"
+else
+ _log "ROBONIX_SDK_PATH not set (no config or empty)"
+fi
+if [ -f /opt/ros/humble/setup.bash ]; then
+ source /opt/ros/humble/setup.bash && _log "sourced ROS2 humble"
+else
+ _log "ROS2 /opt/ros/humble/setup.bash not found"
+fi
+cd "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+_log "PWD=${PWD}"
+
+# Prefer source layout when skill module exists (avoids ros2 run when no colcon install)
+if [ -f navigation_skills_provider/wandering_skill.py ]; then
+ _log "layout=source, using python3 -m navigation_skills_provider.wandering_skill"
+ export PYTHONPATH="${PWD}:${PYTHONPATH}"
+ if [ -n "$ROBONIX_SDK_PATH" ]; then
+ SDK_PY=$(find "$ROBONIX_SDK_PATH/install" -type d -path "*/dist-packages" 2>/dev/null | head -1)
+ if [ -n "$SDK_PY" ]; then
+ export PYTHONPATH="${SDK_PY}:${PYTHONPATH}"
+ _log "PYTHONPATH includes SDK dist-packages: ${SDK_PY}"
+ else
+ _log "SDK dist-packages not found under ROBONIX_SDK_PATH/install, robonix_sdk may fail to import"
+ fi
+ fi
+ _log "exec: python3 -m navigation_skills_provider.wandering_skill"
+ exec python3 -m navigation_skills_provider.wandering_skill
+fi
+if [ -f install/setup.bash ]; then
+ _log "layout=install, using ros2 run navigation_skills_provider wandering_skill"
+ source install/setup.bash && exec ros2 run navigation_skills_provider wandering_skill
+fi
+_log "error: no navigation_skills_provider/wandering_skill.py and no install/setup.bash"
+exit 1
diff --git a/rust/provider/navigation_skills/rbnx/stop_move_to_object.sh b/rust/provider/navigation_skills/rbnx/stop_move_to_object.sh
new file mode 100755
index 0000000..3d7735a
--- /dev/null
+++ b/rust/provider/navigation_skills/rbnx/stop_move_to_object.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+# SPDX-License-Identifier: MulanPSL-2.0
+# Stop Move to Object Skill Script
+#
+# Stop script for move_to_object skill
+# Note: Process will be managed by PID by CLI, this is for additional cleanup if needed
+
+set -e
+
+echo "Move to object skill stop requested (process managed by CLI)"
+exit 0
diff --git a/rust/provider/navigation_skills/rbnx/stop_wandering.sh b/rust/provider/navigation_skills/rbnx/stop_wandering.sh
new file mode 100755
index 0000000..c4c121f
--- /dev/null
+++ b/rust/provider/navigation_skills/rbnx/stop_wandering.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+# SPDX-License-Identifier: MulanPSL-2.0
+# Stop Wandering Skill Script
+#
+# Stop script for wandering skill
+# Note: Process will be managed by PID by CLI, this is for additional cleanup if needed
+
+set -e
+
+echo "Wandering skill stop requested (process managed by CLI)"
+exit 0
diff --git a/rust/provider/navigation_skills/rbnx_manifest.yaml b/rust/provider/navigation_skills/rbnx_manifest.yaml
new file mode 100644
index 0000000..d730950
--- /dev/null
+++ b/rust/provider/navigation_skills/rbnx_manifest.yaml
@@ -0,0 +1,45 @@
+# Robonix Package Manifest
+# This file defines the primitives, services, and skills provided by this package
+# Format version: 2.0 (EAIOS Architecture)
+#
+# Note:
+# - Primitives and services must conform to standard specifications
+# - Skills are flexible and user-defined
+# - JSON fields (input_schema, output_schema, metadata, start_args, status) must be valid JSON strings
+
+package:
+ name: navigation_skills_provider
+ version: 0.0.1
+ description: Navigation skills package providing wandering and move_to_object skills
+ maintainer: wheatfox
+ maintainer_email: wheatfox17@icloud.com
+ license: MulanPSL-2.0
+ build_script: rbnx/build.sh
+
+# Skills provided by this package
+skills:
+ # Wandering skill - randomly wanders and explores the environment
+ - name: skl::wandering
+ type: basic
+ start_topic: /robot1/skill/wandering/start
+ status_topic: /robot1/skill/wandering/status
+ entry: bash rbnx/start_wandering.sh
+ start_args: '{"wander_radius":"number (optional, default 5.0m)"}'
+ status: '{"state":"string","result":"any"}'
+ metadata: '{"domain":"indoor","capability":["navigation"]}'
+ version: 0.0.1
+ start_script: rbnx/start_wandering.sh
+ stop_script: rbnx/stop_wandering.sh
+
+ # Move to object skill - navigates to a specific object nearby, stopping at safe distance
+ - name: skl::move_to_object
+ type: basic
+ start_topic: /robot1/skill/move_to_object/start
+ status_topic: /robot1/skill/move_to_object/status
+ entry: bash rbnx/start_move_to_object.sh
+ start_args: '{"target_object_id":"string (required, UUID of target object)","stop_radius":"number (optional, default 0.5m)"}'
+ status: '{"state":"string","result":"any"}'
+ metadata: '{"domain":"indoor","capability":["navigation","semantic_mapping"]}'
+ version: 0.0.1
+ start_script: rbnx/start_move_to_object.sh
+ stop_script: rbnx/stop_move_to_object.sh
diff --git a/rust/provider/demo_package/resource/demo_rgb_provider b/rust/provider/navigation_skills/resource/navigation_skills_provider
similarity index 100%
rename from rust/provider/demo_package/resource/demo_rgb_provider
rename to rust/provider/navigation_skills/resource/navigation_skills_provider
diff --git a/rust/provider/navigation_skills/setup.py b/rust/provider/navigation_skills/setup.py
new file mode 100644
index 0000000..797c68f
--- /dev/null
+++ b/rust/provider/navigation_skills/setup.py
@@ -0,0 +1,30 @@
+from setuptools import setup, find_packages
+import os
+from glob import glob
+
+package_name = 'navigation_skills_provider'
+
+setup(
+ name=package_name,
+ version='0.0.1',
+ packages=find_packages(),
+ data_files=[
+ ('share/ament_index/resource_index/packages',
+ ['resource/' + package_name]),
+ ('share/' + package_name, ['package.xml']),
+ ('share/' + package_name, ['rbnx_manifest.yaml']),
+ ],
+ install_requires=['setuptools'],
+ zip_safe=True,
+ maintainer='root',
+ maintainer_email='demo@demo.demo',
+ description='Navigation skills package providing wandering and move_to_object skills',
+ license='MulanPSL-2.0',
+ tests_require=['pytest'],
+ entry_points={
+ 'console_scripts': [
+ 'wandering_skill = navigation_skills_provider.wandering_skill:main',
+ 'move_to_object_skill = navigation_skills_provider.move_to_object_skill:main',
+ ],
+ },
+)
diff --git a/rust/provider/tiago_demo_package/.gitignore b/rust/provider/tiago_demo_package/.gitignore
new file mode 100644
index 0000000..dafb7a2
--- /dev/null
+++ b/rust/provider/tiago_demo_package/.gitignore
@@ -0,0 +1,2 @@
+bin/*
+build
diff --git a/rust/provider/tiago_demo_package/README.md b/rust/provider/tiago_demo_package/README.md
new file mode 100644
index 0000000..fd650b3
--- /dev/null
+++ b/rust/provider/tiago_demo_package/README.md
@@ -0,0 +1,45 @@
+# Tiago Simulator Package
+
+Before registering this package or starting programs via recipes, start the simulator and supporting services manually.
+
+First, you need to make sure you've installed this package and `rbnx package list` will show it.
+
+```bash
+rbnx package build tiago_demo_package
+```
+
+## 1. Start Webots simulator
+
+```bash
+cd rust/provider/tiago_demo_package
+./run.sh
+```
+
+## 2. Start Nav2 (new terminal)
+
+```bash
+cd rust/provider/tiago_demo_package/nav2_webots_tiago
+./run.sh
+```
+
+## 3. (Optional) Start RViz2 (new terminal)
+
+```bash
+cd rust/provider/tiago_demo_package
+./start_rviz.sh
+```
+
+## 4. Start robonix-core and register (new terminal)
+
+From the **rust** directory, source the SDK and start robonix-core with environment variables (no CLI flags). Then use `rbnx` to register and deploy.
+
+```bash
+cd rust
+eval $(make source-sdk)
+ROBONIX_WEB_ASSETS_DIR="$(pwd)/robonix-core/web" \
+ROBONIX_WEB_PORT=8000 \
+RUST_LOG=robonix_core=info \
+robonix-core
+```
+
+In another terminal (with `eval $(make source-sdk)` from `rust`): register the recipe, start services, and create tasks. See the main [rust/README.md](../../README.md) for full steps (Step 4–8).
\ No newline at end of file
diff --git a/rust/provider/tiago_demo_package/description.yml b/rust/provider/tiago_demo_package/description.yml
new file mode 100644
index 0000000..7e27eaa
--- /dev/null
+++ b/rust/provider/tiago_demo_package/description.yml
@@ -0,0 +1,6 @@
+name: "webots_tiago"
+version: "1.0.0"
+author: "syswonder"
+start_on_boot: true
+startup_command: "bash run.sh"
+description: "run webots_tiago"
diff --git a/rust/provider/tiago_demo_package/eaios_webots/LICENSE b/rust/provider/tiago_demo_package/eaios_webots/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/rust/provider/tiago_demo_package/eaios_webots/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/rust/provider/tiago_demo_package/eaios_webots/eaios_webots/__init__.py b/rust/provider/tiago_demo_package/eaios_webots/eaios_webots/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/rust/provider/tiago_demo_package/eaios_webots/eaios_webots/my_robot_driver.py b/rust/provider/tiago_demo_package/eaios_webots/eaios_webots/my_robot_driver.py
new file mode 100644
index 0000000..e69de29
diff --git a/rust/provider/tiago_demo_package/eaios_webots/launch/robot_launch.py b/rust/provider/tiago_demo_package/eaios_webots/launch/robot_launch.py
new file mode 100644
index 0000000..c84bb76
--- /dev/null
+++ b/rust/provider/tiago_demo_package/eaios_webots/launch/robot_launch.py
@@ -0,0 +1,135 @@
+import os
+import launch
+from launch import LaunchDescription
+from launch.substitutions import LaunchConfiguration, TextSubstitution, PathJoinSubstitution
+from launch.actions import DeclareLaunchArgument
+from launch_ros.actions import Node
+from ament_index_python.packages import get_package_share_directory
+from webots_ros2_driver.webots_launcher import WebotsLauncher
+from webots_ros2_driver.webots_controller import WebotsController
+from webots_ros2_driver.wait_for_controller_connection import WaitForControllerConnection
+
+
+def generate_launch_description():
+ package_dir = get_package_share_directory('eaios_webots')
+
+ robot_arg = DeclareLaunchArgument(
+ 'robot',
+ default_value=TextSubstitution(text='tiago_webots.urdf'), # Default to the turtlebot_urdf file
+ description='Path to the robot URDF file (relative to the package share directory)'
+ )
+ world_arg = DeclareLaunchArgument(
+ 'world',
+ default_value=TextSubstitution(text='office.wbt'), # Default to the office.wbt file
+ description='Path to the Webots world file (relative to the package share directory)'
+ )
+
+ robot_urdf_file = LaunchConfiguration('robot')
+ world_wbt_file = LaunchConfiguration('world')
+ use_sim_time = LaunchConfiguration('use_sim_time', default=True)
+
+
+ robot_description_path = PathJoinSubstitution([
+ package_dir,
+ 'resource', # Assuming URDF files are in the 'resource' folder
+ robot_urdf_file
+ ])
+ world_description_path = PathJoinSubstitution([
+ package_dir,
+ 'worlds', # Assuming world files are in the 'worlds' folder
+ world_wbt_file
+ ])
+
+ print(f"using robot_path:{robot_description_path}")
+ print(f"using world_path:{world_description_path}")
+
+ webots = WebotsLauncher(
+ world=world_description_path,
+ mode="realtime",
+ ros2_supervisor=True
+ # Other possible Webots parameters, e.g., gui, mode, etc.
+ )
+
+ # ROS control spawners
+ controller_manager_timeout = ['--controller-manager-timeout', '500']
+ controller_manager_prefix = 'python.exe' if os.name == 'nt' else ''
+ diffdrive_controller_spawner = Node(
+ package='controller_manager',
+ executable='spawner',
+ output='screen',
+ prefix=controller_manager_prefix,
+ arguments=['diffdrive_controller'] + controller_manager_timeout,
+ )
+ joint_state_broadcaster_spawner = Node(
+ package='controller_manager',
+ executable='spawner',
+ output='screen',
+ prefix=controller_manager_prefix,
+ arguments=['joint_state_broadcaster'] + controller_manager_timeout,
+ )
+ ros_control_spawners = [diffdrive_controller_spawner, joint_state_broadcaster_spawner]
+
+
+ robot_state_publisher = Node(
+ package='robot_state_publisher',
+ executable='robot_state_publisher',
+ output='screen',
+ parameters=[{
+ 'robot_description': ''
+ }],
+ )
+
+ footprint_publisher = Node(
+ package='tf2_ros',
+ executable='static_transform_publisher',
+ output='screen',
+ arguments=['0', '0', '0', '0', '0', '0', 'base_link', 'base_footprint'],
+ )
+
+
+ use_twist_stamped = 'ROS_DISTRO' in os.environ and (os.environ['ROS_DISTRO'] in ['rolling', 'jazzy', 'kilted'])
+ if use_twist_stamped:
+ mappings = [('/diffdrive_controller/cmd_vel', '/cmd_vel'), ('/diffdrive_controller/odom', '/odom')]
+ else:
+ mappings = [('/diffdrive_controller/cmd_vel_unstamped', '/cmd_vel'), ('/diffdrive_controller/odom', '/odom')]
+ ros2_control_params = os.path.join(package_dir, 'resource', 'ros2_control.yml')
+ my_robot_driver = WebotsController(
+ robot_name='my_robot', # Ensure this name matches the robot node name in the Webots world.
+ parameters=[
+ {'robot_description': robot_description_path, # This robot_description is typically for the Webots driver.
+ 'use_sim_time': use_sim_time,
+ 'set_robot_state_publisher': True}, # Set to True if WebotsController should launch its own RSP.
+ ros2_control_params
+ ],
+ remappings=mappings,
+ respawn=True
+ )
+
+ waiting_nodes = WaitForControllerConnection(
+ target_driver=my_robot_driver,
+ nodes_to_start=ros_control_spawners
+ )
+
+ return LaunchDescription([
+ robot_arg,
+ world_arg,
+ DeclareLaunchArgument(
+ 'use_sim_time',
+ default_value=TextSubstitution(text='True'),
+ description='Use simulation (Webots) clock if true'
+ ),
+
+ webots,
+ webots._supervisor,
+ robot_state_publisher, # Ensure robot_state_publisher starts before my_robot_driver if my_robot_driver depends on it.
+ footprint_publisher,
+ my_robot_driver,
+ waiting_nodes,
+ launch.actions.RegisterEventHandler(
+ event_handler=launch.event_handlers.OnProcessExit(
+ target_action=webots,
+ on_exit=[launch.actions.EmitEvent(event=launch.events.Shutdown())],
+ )
+ )
+ ])
+
diff --git a/rust/provider/tiago_demo_package/eaios_webots/package.xml b/rust/provider/tiago_demo_package/eaios_webots/package.xml
new file mode 100644
index 0000000..5057bf3
--- /dev/null
+++ b/rust/provider/tiago_demo_package/eaios_webots/package.xml
@@ -0,0 +1,22 @@
+
+
+
+ eaios_webots
+ 0.0.0
+ TODO: Package description
+ vuken
+ MulanPSL-2.0
+
+ rclpy
+ geometry_msgs
+ webots_ros2_driver
+
+ ament_copyright
+ ament_flake8
+ ament_pep257
+ python3-pytest
+
+
+ ament_python
+
+
diff --git a/rust/provider/tiago_demo_package/eaios_webots/resource/eaios_webots b/rust/provider/tiago_demo_package/eaios_webots/resource/eaios_webots
new file mode 100644
index 0000000..e69de29
diff --git a/rust/provider/tiago_demo_package/eaios_webots/resource/ros2_control.yml b/rust/provider/tiago_demo_package/eaios_webots/resource/ros2_control.yml
new file mode 100644
index 0000000..fc787bb
--- /dev/null
+++ b/rust/provider/tiago_demo_package/eaios_webots/resource/ros2_control.yml
@@ -0,0 +1,38 @@
+controller_manager:
+ ros__parameters:
+ update_rate: 50
+
+ diffdrive_controller:
+ type: diff_drive_controller/DiffDriveController
+
+ joint_state_broadcaster:
+ type: joint_state_broadcaster/JointStateBroadcaster
+
+diffdrive_controller:
+ ros__parameters:
+ left_wheel_names: ["wheel_left_joint"]
+ right_wheel_names: ["wheel_right_joint"]
+
+ wheel_separation: 0.404
+ wheel_radius: 0.0985
+
+ # The real separation between wheels is not resulting in a perfect odometry
+ wheel_separation_multiplier: 1.089
+
+ use_stamped_vel: false
+ base_frame_id: "base_link"
+
+joint_state_broadcaster:
+ ros__parameters:
+ extra_joints:
+ - CASTER_WHEEL_FRONT_LEFT_JOINT
+ - CASTER_WHEEL_FRONT_RIGHT_JOINT
+ - CASTER_WHEEL_BACK_LEFT_JOINT
+ - CASTER_WHEEL_BACK_RIGHT_JOINT
+ - SMALL_WHEEL_JOINT
+ - SMALL_WHEEL_JOINT_0
+ - SMALL_WHEEL_JOINT_1
+ - SMALL_WHEEL_JOINT_2
+ - head_1_joint
+ - head_2_joint
+ - torso_lift_joint
diff --git a/rust/provider/tiago_demo_package/eaios_webots/resource/tiago_webots.urdf b/rust/provider/tiago_demo_package/eaios_webots/resource/tiago_webots.urdf
new file mode 100644
index 0000000..e21c2db
--- /dev/null
+++ b/rust/provider/tiago_demo_package/eaios_webots/resource/tiago_webots.urdf
@@ -0,0 +1,49 @@
+
+
+
+
+
+ true
+ 10
+ /scanner
+ true
+ hokuyo
+
+
+
+
+ true
+ /head_front_camera/rgb
+ /image_raw
+ true
+ head_front_camera_rgb_optical_frame
+
+
+
+
+ true
+ /head_front_camera/depth_registered
+ /image_raw
+ /points
+ true
+ head_front_camera_depth_optical_frame
+
+
+
+
+
+
+
+
+ webots_ros2_control::Ros2ControlSystem
+
+
+
+
+
+
+
+
+
+
+
diff --git a/rust/provider/tiago_demo_package/eaios_webots/resource/turtlebot_webots.urdf b/rust/provider/tiago_demo_package/eaios_webots/resource/turtlebot_webots.urdf
new file mode 100644
index 0000000..e6c7eb4
--- /dev/null
+++ b/rust/provider/tiago_demo_package/eaios_webots/resource/turtlebot_webots.urdf
@@ -0,0 +1,46 @@
+
+
+
+
+
+ true
+ 5
+ /scan
+ false
+ LDS-01
+
+
+
+
+ true
+ 20
+ /imu
+ false
+ imu_link
+ inertial_unit
+ gyro
+ accelerometer
+
+
+
+
+
+
+ someValue
+
+
+
+
+
+ webots_ros2_control::Ros2ControlSystem
+
+
+
+
+
+
+
+
+
+
+
diff --git a/rust/provider/tiago_demo_package/eaios_webots/setup.cfg b/rust/provider/tiago_demo_package/eaios_webots/setup.cfg
new file mode 100644
index 0000000..2da324d
--- /dev/null
+++ b/rust/provider/tiago_demo_package/eaios_webots/setup.cfg
@@ -0,0 +1,4 @@
+[develop]
+script_dir=$base/lib/eaios_webots
+[install]
+install_scripts=$base/lib/eaios_webots
diff --git a/rust/provider/tiago_demo_package/eaios_webots/setup.py b/rust/provider/tiago_demo_package/eaios_webots/setup.py
new file mode 100644
index 0000000..ce87c12
--- /dev/null
+++ b/rust/provider/tiago_demo_package/eaios_webots/setup.py
@@ -0,0 +1,28 @@
+from setuptools import setup
+
+package_name = 'eaios_webots'
+data_files = []
+data_files.append(('share/ament_index/resource_index/packages', ['resource/' + package_name]))
+data_files.append(('share/' + package_name + '/launch', ['launch/robot_launch.py']))
+data_files.append(('share/' + package_name + '/worlds', ['worlds/office.wbt']))
+data_files.append(('share/' + package_name + '/resource', ['resource/tiago_webots.urdf','resource/ros2_control.yml']))
+data_files.append(('share/' + package_name, ['package.xml']))
+
+setup(
+ name=package_name,
+ version='0.0.0',
+ packages=[package_name],
+ data_files=data_files,
+ install_requires=['setuptools'],
+ zip_safe=True,
+ maintainer='user',
+ maintainer_email='user.name@mail.com',
+ description='TODO: Package description',
+ license='TODO: License declaration',
+ tests_require=['pytest'],
+ entry_points={
+ 'console_scripts': [
+ 'my_robot_driver = eaios_webots.my_robot_driver:main',
+ ],
+ },
+)
diff --git a/rust/provider/tiago_demo_package/eaios_webots/test/test_copyright.py b/rust/provider/tiago_demo_package/eaios_webots/test/test_copyright.py
new file mode 100644
index 0000000..97a3919
--- /dev/null
+++ b/rust/provider/tiago_demo_package/eaios_webots/test/test_copyright.py
@@ -0,0 +1,25 @@
+# Copyright 2015 Open Source Robotics Foundation, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from ament_copyright.main import main
+import pytest
+
+
+# Remove the `skip` decorator once the source file(s) have a copyright header
+@pytest.mark.skip(reason='No copyright header has been placed in the generated source file.')
+@pytest.mark.copyright
+@pytest.mark.linter
+def test_copyright():
+ rc = main(argv=['.', 'test'])
+ assert rc == 0, 'Found errors'
diff --git a/rust/provider/tiago_demo_package/eaios_webots/test/test_flake8.py b/rust/provider/tiago_demo_package/eaios_webots/test/test_flake8.py
new file mode 100644
index 0000000..27ee107
--- /dev/null
+++ b/rust/provider/tiago_demo_package/eaios_webots/test/test_flake8.py
@@ -0,0 +1,25 @@
+# Copyright 2017 Open Source Robotics Foundation, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from ament_flake8.main import main_with_errors
+import pytest
+
+
+@pytest.mark.flake8
+@pytest.mark.linter
+def test_flake8():
+ rc, errors = main_with_errors(argv=[])
+ assert rc == 0, \
+ 'Found %d code style errors / warnings:\n' % len(errors) + \
+ '\n'.join(errors)
diff --git a/rust/provider/tiago_demo_package/eaios_webots/test/test_pep257.py b/rust/provider/tiago_demo_package/eaios_webots/test/test_pep257.py
new file mode 100644
index 0000000..b234a38
--- /dev/null
+++ b/rust/provider/tiago_demo_package/eaios_webots/test/test_pep257.py
@@ -0,0 +1,23 @@
+# Copyright 2015 Open Source Robotics Foundation, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from ament_pep257.main import main
+import pytest
+
+
+@pytest.mark.linter
+@pytest.mark.pep257
+def test_pep257():
+ rc = main(argv=['.', 'test'])
+ assert rc == 0, 'Found code style errors / warnings'
diff --git a/rust/provider/tiago_demo_package/eaios_webots/worlds/office.wbt b/rust/provider/tiago_demo_package/eaios_webots/worlds/office.wbt
new file mode 100644
index 0000000..4e448c1
--- /dev/null
+++ b/rust/provider/tiago_demo_package/eaios_webots/worlds/office.wbt
@@ -0,0 +1,693 @@
+#VRML_SIM R2023b utf8
+
+EXTERNPROTO "https://raw.githubusercontent.com/cyberbotics/webots/develop/projects/objects/backgrounds/protos/TexturedBackground.proto"
+EXTERNPROTO "https://raw.githubusercontent.com/cyberbotics/webots/develop/projects/objects/floors/protos/Floor.proto"
+EXTERNPROTO "https://raw.githubusercontent.com/cyberbotics/webots/develop/projects/appearances/protos/Parquetry.proto"
+EXTERNPROTO "https://raw.githubusercontent.com/cyberbotics/webots/develop/projects/objects/lights/protos/CeilingLight.proto"
+EXTERNPROTO "https://raw.githubusercontent.com/cyberbotics/webots/develop/projects/objects/apartment_structure/protos/Wall.proto"
+EXTERNPROTO "https://raw.githubusercontent.com/cyberbotics/webots/develop/projects/appearances/protos/Roughcast.proto"
+EXTERNPROTO "https://raw.githubusercontent.com/cyberbotics/webots/develop/projects/objects/apartment_structure/protos/Window.proto"
+EXTERNPROTO "https://raw.githubusercontent.com/cyberbotics/webots/develop/projects/appearances/protos/MattePaint.proto"
+EXTERNPROTO "https://raw.githubusercontent.com/cyberbotics/webots/develop/projects/objects/apartment_structure/protos/Door.proto"
+EXTERNPROTO "https://raw.githubusercontent.com/cyberbotics/webots/develop/projects/objects/living_room_furniture/protos/Sofa.proto"
+EXTERNPROTO "https://raw.githubusercontent.com/cyberbotics/webots/develop/projects/objects/plants/protos/PottedTree.proto"
+EXTERNPROTO "https://raw.githubusercontent.com/cyberbotics/webots/develop/projects/objects/cabinet/protos/Cabinet.proto"
+EXTERNPROTO "https://raw.githubusercontent.com/cyberbotics/webots/develop/projects/objects/cabinet/protos/CabinetHandle.proto"
+EXTERNPROTO "https://raw.githubusercontent.com/cyberbotics/webots/develop/projects/appearances/protos/GlossyPaint.proto"
+EXTERNPROTO "https://raw.githubusercontent.com/cyberbotics/webots/develop/projects/objects/solids/protos/SolidBox.proto"
+EXTERNPROTO "https://raw.githubusercontent.com/cyberbotics/webots/develop/projects/objects/kitchen/components/protos/Sink.proto"
+EXTERNPROTO "https://raw.githubusercontent.com/cyberbotics/webots/develop/projects/objects/tables/protos/Table.proto"
+EXTERNPROTO "https://raw.githubusercontent.com/cyberbotics/webots/develop/projects/appearances/protos/VarnishedPine.proto"
+EXTERNPROTO "https://raw.githubusercontent.com/cyberbotics/webots/develop/projects/objects/computers/protos/Monitor.proto"
+EXTERNPROTO "https://raw.githubusercontent.com/cyberbotics/webots/develop/projects/objects/computers/protos/Keyboard.proto"
+EXTERNPROTO "https://raw.githubusercontent.com/cyberbotics/webots/develop/projects/objects/chairs/protos/OfficeChair.proto"
+EXTERNPROTO "https://raw.githubusercontent.com/cyberbotics/webots/develop/projects/robots/pal_robotics/tiago_lite/protos/TiagoLite.proto"
+
+WorldInfo {
+ info [
+ "An office break room, surrounded by office desks."
+ ]
+ basicTimeStep 20
+}
+Viewpoint {
+ fieldOfView 0.5
+ orientation 0.3179803094444856 0.038784817888461866 -0.9473036792428013 2.9187109824608912
+ position 27.15248440605859 6.083643329439582 22.02228189083543
+ near 1
+ follow "TIAGo Iron"
+ followType "None"
+ followSmoothness 0
+}
+TexturedBackground {
+ texture "entrance_hall"
+ luminosity 0.5
+ skybox FALSE
+}
+Floor {
+ translation -0.93 0.243 0
+ size 7.7 12.86
+ appearance Parquetry {
+ type "light strip"
+ textureTransform TextureTransform {
+ rotation 1.57
+ scale 0.4 0.4
+ }
+ }
+}
+CeilingLight {
+ translation 0.87 4.78 2.5
+ rotation 1 0 0 4.692820414042842e-06
+ bulbColor 0.913725 0.72549 0.431373
+ supportColor 0.533333 0.541176 0.521569
+ pointLightIntensity 3
+ pointLightCastShadows TRUE
+}
+CeilingLight {
+ translation -2.74 4.78 2.5
+ rotation 1 0 0 4.692820414042842e-06
+ name "ceiling light(5)"
+ bulbColor 0.913725 0.72549 0.431373
+ supportColor 0.533333 0.541176 0.521569
+ pointLightIntensity 3
+ castShadows FALSE
+}
+CeilingLight {
+ translation 0.87 0.31 2.5
+ rotation 1 0 0 4.692820414042842e-06
+ name "ceiling light(1)"
+ bulbColor 0.913725 0.72549 0.431373
+ supportColor 0.533333 0.541176 0.521569
+ pointLightIntensity 3
+ castShadows FALSE
+}
+CeilingLight {
+ translation -2.74 -3.52 2.5
+ rotation 1 0 0 4.692820414042842e-06
+ name "ceiling light(3)"
+ bulbColor 0.913725 0.72549 0.431373
+ supportColor 0.533333 0.541176 0.521569
+ pointLightIntensity 3
+ pointLightCastShadows TRUE
+ castShadows FALSE
+}
+Wall {
+ translation -2.92 -2.65 0
+ size 3.5 0.05 0.62
+ appearance DEF SMALL_WALLS Roughcast {
+ colorOverride 0.890196 0.803922 1
+ textureTransform TextureTransform {
+ scale 1 2.4
+ }
+ }
+}
+Wall {
+ translation 0.77 2.65 0
+ name "wall(1)"
+ size 3.55 0.05 0.62
+ appearance USE SMALL_WALLS
+}
+Wall {
+ translation 2.52 0 0
+ rotation 7.19232e-09 7.19235e-09 -1 -1.5707953071795862
+ name "wall(2)"
+ size 5.35 0.05 0.62
+ appearance USE SMALL_WALLS
+}
+Wall {
+ translation -0.979 3.026 0
+ rotation 0 0 1 1.5708
+ name "wall(7)"
+ size 0.7 0.05 0.62
+ appearance USE SMALL_WALLS
+}
+Wall {
+ translation -0.92 -6.17 0
+ rotation -0.788631751825513 -5.672108215044546e-09 -0.6148658065079098 -5.307179586466759e-06
+ name "wall(A)"
+ size 7.7 0.06 2.5
+ appearance DEF SIDE_WALLS Roughcast {
+ textureTransform TextureTransform {
+ scale 3 3
+ }
+ }
+}
+Wall {
+ translation 2.9 -5.94 0
+ rotation 0 0 1 1.5708
+ name "wall(B)1"
+ size 0.4 0.06 2.5
+ appearance DEF INTER_WALLS Roughcast {
+ textureTransform TextureTransform {
+ scale 0.3 2.2
+ }
+ }
+}
+Window {
+ translation 2.9 -5.19 0
+ rotation -1.1201004593060733e-08 -0.005401822215060023 0.9999854100519451 -5.307179586466759e-06
+ name "window(B)2"
+ size 0.06 1.1 2.5
+ bottomWallHeight 1
+ frameSize 0.02 0.05 0.02
+ windowSillSize 0.3 0.05
+ frameAppearance DEF WINDOWS_WOOD MattePaint {
+ baseColor 0.133333 0.0666667 0
+ }
+}
+Window {
+ translation 2.9 -3.84 0
+ rotation 2.9524886867359215e-08 0.014173193695777315 0.9998995552456563 1.01503e-06
+ name "window(B)3"
+ size 0.06 1.6 2.5
+ bottomWallHeight 1
+ frameSize 0.02 0.05 0.02
+ windowSillSize 0.3 0.05
+ frameAppearance USE WINDOWS_WOOD
+}
+Wall {
+ translation 2.9 -2.84 0
+ rotation 0 0 1 1.5708
+ name "wall(B)4"
+ size 0.4 0.06 2.5
+ appearance USE INTER_WALLS
+}
+Window {
+ translation 2.9 -2.09 0
+ rotation 2.9524886867359215e-08 0.014173193695777315 0.9998995552456563 1.01503e-06
+ name "window(B)5"
+ size 0.06 1.1 2.5
+ bottomWallHeight 1
+ frameSize 0.02 0.05 0.02
+ windowSillSize 0.3 0.05
+ frameAppearance USE WINDOWS_WOOD
+}
+Window {
+ translation 2.9 -0.74 0
+ rotation 2.9524886867359215e-08 0.014173193695777315 0.9998995552456563 1.01503e-06
+ name "window(B)6"
+ size 0.06 1.6 2.5
+ bottomWallHeight 1
+ frameSize 0.02 0.05 0.02
+ windowSillSize 0.3 0.05
+ frameAppearance USE WINDOWS_WOOD
+}
+Wall {
+ translation 2.9 0.26 0
+ rotation 0 0 1 1.5708
+ name "wall(B)7"
+ size 0.4 0.06 2.5
+ appearance USE INTER_WALLS
+}
+Window {
+ translation 2.9 1 0
+ rotation 0.12721696286162107 0.4975208547590069 0.8580727495032879 1.01503e-06
+ name "window(B)8"
+ size 0.06 1.1 2.5
+ bottomWallHeight 1
+ frameSize 0.02 0.05 0.02
+ windowSillSize 0.3 0.05
+ frameAppearance USE WINDOWS_WOOD
+}
+Window {
+ translation 2.9 2.35 0
+ rotation 2.9524886867359215e-08 0.014173193695777315 0.9998995552456563 1.01503e-06
+ name "window(B)9"
+ size 0.06 1.6 2.5
+ bottomWallHeight 1
+ frameSize 0.02 0.05 0.02
+ windowSillSize 0.3 0.05
+ frameAppearance USE WINDOWS_WOOD
+}
+Wall {
+ translation 2.9 3.35 0
+ rotation 0 0 1 1.5708
+ name "wall(B)10"
+ size 0.4 0.06 2.5
+ appearance USE INTER_WALLS
+}
+Window {
+ translation 2.9 4.1 0
+ rotation 2.9524886867359215e-08 0.014173193695777315 0.9998995552456563 1.01503e-06
+ name "window(B)11"
+ size 0.06 1.1 2.5
+ bottomWallHeight 1
+ frameSize 0.02 0.05 0.02
+ windowSillSize 0.3 0.05
+ frameAppearance USE WINDOWS_WOOD
+}
+Window {
+ translation 2.9 5.45 0
+ rotation 2.9524886867359215e-08 0.014173193695777315 0.9998995552456563 1.01503e-06
+ name "window(B)12"
+ size 0.06 1.6 2.5
+ bottomWallHeight 1
+ frameSize 0.02 0.05 0.02
+ windowSillSize 0.3 0.05
+ frameAppearance USE WINDOWS_WOOD
+}
+Wall {
+ translation 2.9 6.45 0
+ rotation 0 0 1 1.5708
+ name "wall(B)13"
+ size 0.4 0.06 2.5
+ appearance USE INTER_WALLS
+}
+Wall {
+ translation -0.92 6.68 0
+ rotation 0 0 1 3.14159
+ name "wall(C)"
+ size 7.7 0.06 2.5
+ appearance USE SIDE_WALLS
+}
+Wall {
+ translation -4.8 5.71 0
+ rotation 0 0 1 -1.5708
+ name "wall(D)1"
+ size 2 0.06 2.5
+}
+Door {
+ translation -4.8 4.21 0
+ rotation -2.827660768165793e-11 0.003931491068033687 -0.9999922716591273 -5.307179586466759e-06
+ size 0.06 1 2.5
+ frameSize 0.05 0.05 0.01
+ frameAppearance MattePaint {
+ baseColor 0.133333 0.0666667 0
+ }
+}
+Wall {
+ translation -4.8 -1.24 0
+ rotation 2.9524886867359215e-08 0.014173193695777315 0.9998995552456563 1.01503e-06
+ name "wall(D)2"
+ size 0.06 9.9 2.5
+}
+Sofa {
+ translation 2 1.22999 0
+ rotation 9.22314e-15 7.19235e-09 -1 -3.1415853071795863
+}
+PottedTree {
+ translation 2.15 2.38 0
+ rotation 2.3464099999870814e-06 -2.3464099999870814e-06 0.9999999999944944 1.5708
+}
+PottedTree {
+ translation -1.42 6.23 0
+ rotation 2.3464099999870814e-06 -2.3464099999870814e-06 0.9999999999944944 1.5708
+ name "potted tree(1)"
+}
+PottedTree {
+ translation 2.17 -5.78 0
+ rotation -2.3464199999870813e-06 2.3464199999870813e-06 -0.9999999999944944 -1.5707953071795862
+ name "potted tree(2)"
+}
+Cabinet {
+ translation 2.49 -0.01 0
+ rotation 7.19233e-09 -1.49483e-14 1 3.14159
+ outerThickness 0.02
+ rowsHeights [
+ 0.3, 0.4, 0.3, 0.3
+ ]
+ columnsWidths [
+ 0.6
+ ]
+ layout [
+ "Drawer (1, 1, 1, 1, 3.5)"
+ "Shelf (1, 4, 1, 0)"
+ "Shelf (1, 3, 1, 0)"
+ "Shelf (1, 2, 1, 0)"
+ "Shelf (1, 1, 1, 1)"
+ ]
+ handle CabinetHandle {
+ handleColor 0.533333 0.541176 0.521569
+ }
+ primaryAppearance MattePaint {
+ baseColor 0.729412 0.741176 0.713725
+ }
+ secondaryAppearance GlossyPaint {
+ baseColor 0.643137 0 0
+ }
+}
+Cabinet {
+ translation -0.22 2.62 0
+ rotation -2.3464099999870814e-06 -2.3464099999870814e-06 0.9999999999944944 -1.5707953071795862
+ name "cabinet(1)"
+ outerThickness 0.02
+ rowsHeights [
+ 0.1, 0.25
+ ]
+ columnsWidths [
+ 0.5, 0.5
+ ]
+ layout [
+ "Drawer (2, 2, 1, 1, 1.5)"
+ "Drawer (1, 2, 1, 1, 1.5)"
+ "Shelf (1, 1, 0, 2)"
+ "Shelf (2, 2, 1, 0)"
+ "Shelf (1, 2, 1, 0)"
+ "Shelf (1, 1, 0, 1)"
+ ]
+ handle CabinetHandle {
+ handleColor 0.533333 0.541176 0.521569
+ }
+ primaryAppearance MattePaint {
+ baseColor 0.729412 0.741176 0.713725
+ }
+ secondaryAppearance GlossyPaint {
+ baseColor 0.643137 0 0
+ }
+}
+Cabinet {
+ translation 0.7 6.65 1.6
+ rotation 2.7023499999809715e-06 2.6039199999816646e-06 0.9999999999929585 -1.5707953071795862
+ name "cabinet(2)"
+ outerThickness 0.02
+ rowsHeights [
+ 0.8
+ ]
+ columnsWidths [
+ 0.8, 0.8, 0.8, 0.8
+ ]
+ layout [
+ "Drawer (1, 1, 1, 1, 1.5)"
+ "Drawer (2, 1, 1, 1, 1.5)"
+ "Drawer (3, 1, 1, 1, 1.5)"
+ "Drawer (4, 1, 1, 1, 1.5)"
+ "Shelf (1, 1, 0, 1)"
+ "Shelf (2, 1, 0, 1)"
+ "Shelf (3, 1, 0, 1)"
+ ]
+ handle CabinetHandle {
+ handleColor 0.533333 0.541176 0.521569
+ }
+ primaryAppearance MattePaint {
+ baseColor 0.729412 0.741176 0.713725
+ }
+ secondaryAppearance MattePaint {
+ baseColor 0.666667 0.333333 0
+ }
+}
+Cabinet {
+ translation 0.7 -6.14 1.6
+ rotation -1.67821e-08 1.67821e-08 -1 -1.5707953071795862
+ name "cabinet(3)"
+ outerThickness 0.02
+ rowsHeights [
+ 0.8
+ ]
+ columnsWidths [
+ 0.8, 0.8, 0.8, 0.8
+ ]
+ layout [
+ "Drawer (1, 1, 1, 1, 1.5)"
+ "Drawer (2, 1, 1, 1, 1.5)"
+ "Drawer (3, 1, 1, 1, 1.5)"
+ "Drawer (4, 1, 1, 1, 1.5)"
+ "Shelf (1, 1, 0, 1)"
+ "Shelf (2, 1, 0, 1)"
+ "Shelf (3, 1, 0, 1)"
+ ]
+ handle CabinetHandle {
+ handleColor 0.533333 0.541176 0.521569
+ }
+ primaryAppearance MattePaint {
+ baseColor 0.729412 0.741176 0.713725
+ }
+ secondaryAppearance MattePaint {
+ baseColor 0.666667 0.333333 0
+ }
+}
+Cabinet {
+ translation 2.49 -0.66 0
+ rotation 7.19233e-09 -1.49483e-14 1 3.14159
+ name "cabinet(5)"
+ outerThickness 0.02
+ rowsHeights [
+ 0.7, 0.3, 0.3
+ ]
+ columnsWidths [
+ 0.6
+ ]
+ layout [
+ "LeftSidedDoor (1, 1, 1, 1, 1.5)"
+ "Shelf (1, 3, 1, 0)"
+ "Shelf (1, 2, 1, 0)"
+ "Shelf (1, 1, 1, 1)"
+ ]
+ handle CabinetHandle {
+ handleColor 0.533333 0.541176 0.521569
+ }
+ primaryAppearance MattePaint {
+ baseColor 0.729412 0.741176 0.713725
+ }
+ secondaryAppearance GlossyPaint {
+ baseColor 0.643137 0 0
+ }
+}
+Cabinet {
+ translation 2.5 -1.52 0.1
+ rotation 7.19233e-09 -1.49483e-14 1 3.14159
+ name "cabinet(6)"
+ outerThickness 0.02
+ rowsHeights [
+ 0.22, 0.22, 0.22
+ ]
+ columnsWidths [
+ 0.5, 0.5
+ ]
+ layout [
+ "LeftSidedDoor (2, 1, 1, 3, 1.5)"
+ "LeftSidedDoor (1, 1, 1, 3, 1.5)"
+ "Shelf (1, 1, 0, 3)"
+ "Shelf (2, 1, 0, 3)"
+ ]
+ handle CabinetHandle {
+ translation 0 0.26 0.02
+ handleLength 0.1
+ handleRadius 0.008
+ handleColor 0.427451 0.513725 0.533333
+ }
+ primaryAppearance MattePaint {
+ baseColor 0.94667 0.925551 0.852003
+ }
+ secondaryAppearance MattePaint {
+ baseColor 0.94667 0.925551 0.852003
+ }
+}
+Cabinet {
+ translation 2.5 -2.31 0.1
+ rotation 7.19233e-09 -1.49483e-14 1 3.14159
+ name "cabinet(7)"
+ outerThickness 0.02
+ rowsHeights [
+ 0.22, 0.22, 0.22
+ ]
+ columnsWidths [
+ 0.5
+ ]
+ layout [
+ "Drawer (1, 3, 1, 1, 1)"
+ "Drawer (1, 2, 1, 1, 1)"
+ "Drawer (1, 1, 1, 1, 1)"
+ "Shelf (1, 1, 0, 3)"
+ ]
+ handle CabinetHandle {
+ translation 0.02 0.01 0
+ handleLength 0.1
+ handleRadius 0.008
+ handleColor 0.427451 0.513725 0.533333
+ }
+ primaryAppearance MattePaint {
+ baseColor 0.94667 0.925551 0.852003
+ }
+ secondaryAppearance MattePaint {
+ baseColor 0.94667 0.925551 0.852003
+ }
+}
+SolidBox {
+ translation 2.25 -1.79 0.05
+ rotation 0.577348855372322 0.577350976096979 0.577350976096979 2.094397223120449
+ size 1.58 0.1 0.5
+ appearance PBRAppearance {
+ baseColor 0.8 0.8 0.8
+ roughness 0.5
+ metalness 0
+ textureTransform TextureTransform {
+ scale 4 4
+ }
+ }
+}
+Sink {
+ translation 2.32 -2.27 0.835
+ rotation -7.19233e-09 -1.50172e-14 1 3.14159
+}
+Table {
+ translation 1.66 3.18 0
+ rotation 2.3464099999870814e-06 -2.3464099999870814e-06 0.9999999999944944 1.5708
+ size 1 1.6 0.74
+ feetSize 0.05 0.05
+ trayAppearance DEF TABLE_WOOD VarnishedPine {
+ textureTransform TextureTransform {
+ scale 10 10
+ }
+ }
+ legAppearance MattePaint {
+ baseColor 0.2 0.2 0.2
+ }
+}
+Table {
+ translation 1.66 6.14 0
+ rotation -2.3464099999870814e-06 -2.3464099999870814e-06 0.9999999999944944 -1.5707953071795862
+ name "table(1)"
+ size 1 1.6 0.74
+ feetSize 0.05 0.05
+ legAppearance MattePaint {
+ baseColor 0.2 0.2 0.2
+ }
+}
+Table {
+ translation -0.05 3.18 0
+ rotation 2.3464099999870814e-06 -2.3464099999870814e-06 0.9999999999944944 1.5708
+ name "table(3)"
+ feetSize 0.05 0.05
+ legAppearance MattePaint {
+ baseColor 0.2 0.2 0.2
+ }
+}
+Table {
+ translation -0.05 6.14 0
+ rotation -2.3464099999870814e-06 -2.3464099999870814e-06 0.9999999999944944 -1.5707953071795862
+ name "table(4)"
+ feetSize 0.05 0.05
+ legAppearance MattePaint {
+ baseColor 0.2 0.2 0.2
+ }
+}
+Table {
+ translation -2.03 -3.18 0
+ rotation -2.3464099999870814e-06 -2.3464099999870814e-06 0.9999999999944944 -1.5707953071795862
+ name "table(5)"
+ feetSize 0.05 0.05
+ legAppearance MattePaint {
+ baseColor 0.2 0.2 0.2
+ }
+}
+Table {
+ translation -3.83 -3.18 0
+ rotation -2.3464099999870814e-06 -2.3464099999870814e-06 0.9999999999944944 -1.5707953071795862
+ name "table(6)"
+ feetSize 0.05 0.05
+ legAppearance MattePaint {
+ baseColor 0.2 0.2 0.2
+ }
+}
+Monitor {
+ translation -3.82817 -3.02267 0.76
+ rotation 1.50576e-07 -1.47902e-07 1 -1.8325853071795866
+}
+Monitor {
+ translation 1.79048 6.47928 0.76
+ rotation -7.19236e-09 -6.30754e-09 1 -1.701685307179586
+ name "monitor(1)"
+}
+Monitor {
+ translation -4.33723 -3.01523 0.76
+ rotation 7.19236e-09 9.3732e-09 -1 1.309
+ name "monitor(2)"
+}
+Monitor {
+ translation 1.29031 6.47723 0.76
+ rotation 7.19237e-09 8.20135e-09 -1 1.43989
+ name "monitor(3)"
+}
+Monitor {
+ translation -1.65969 -2.84277 0.76
+ rotation -7.19233e-09 -6.30755e-09 1 -1.7016953071795866
+ name "monitor(6)"
+}
+Monitor {
+ translation -2.14969 -2.84277 0.76
+ rotation 7.19237e-09 8.20135e-09 -1 1.43989
+ name "monitor(7)"
+}
+Monitor {
+ translation 1.79048 2.87928 0.76
+ rotation -7.19212e-09 6.30732e-09 1 1.70169
+ name "monitor(8)"
+}
+Monitor {
+ translation 1.29031 2.87723 0.76
+ rotation -7.19209e-09 8.201e-09 1 1.4399
+ name "monitor(9)"
+}
+Monitor {
+ translation 0.18031 2.87723 0.76
+ rotation -7.19215e-09 6.30735e-09 1 1.70169
+ name "monitor(10)"
+}
+Monitor {
+ translation -0.35969 2.87723 0.76
+ rotation -7.19211e-09 8.20104e-09 1 1.4399
+ name "monitor(11)"
+}
+Keyboard {
+ translation -4.02533 -3.20123 0.74
+ rotation -7.19237e-09 8.20127e-09 -1 -1.4398953071795866
+}
+Keyboard {
+ translation 1.57 6.05 0.74
+ rotation 2.3464099999870814e-06 -2.3464099999870814e-06 0.9999999999944944 1.5708
+ name "keyboard(1)"
+}
+Keyboard {
+ translation -1.91 -3.26 0.74
+ rotation -7.19233e-09 -7.19233e-09 1 -1.5707953071795862
+ name "keyboard(3)"
+}
+Keyboard {
+ translation 1.5267 2.98306 0.74
+ rotation 7.19243e-09 5.51899e-09 1 -1.8325953071795862
+ name "keyboard(4)"
+}
+Keyboard {
+ translation -0.1 3.07 0.74
+ rotation -2.3464099999870814e-06 -2.3464099999870814e-06 0.9999999999944944 -1.5707953071795862
+ name "keyboard(5)"
+}
+OfficeChair {
+ translation 0.0449998 4.01794 0
+ rotation 7.19235e-09 4.15252e-09 1 -2.094395307179586
+}
+OfficeChair {
+ translation 1.53 3.94 0
+ rotation -2.3464099999870814e-06 -2.3464099999870814e-06 0.9999999999944944 -1.5707953071795862
+ name "office chair(1)"
+}
+OfficeChair {
+ translation 1.48338 5.36388 0
+ rotation 7.19235e-09 -5.5189e-09 1 1.83259
+ name "office chair(2)"
+}
+OfficeChair {
+ translation -3.88082 -4.17593 0
+ rotation -7.19235e-09 7.19237e-09 -1 -1.5707953071795862
+ name "office chair(3)"
+}
+OfficeChair {
+ translation -1.85 -4.07 0
+ rotation 7.19235e-09 -2.97918e-09 1 2.35619
+ name "office chair(5)"
+}
+TiagoLite {
+ translation 0.962153 -4.80117 0.235361
+ rotation -0.006363227570975 -0.002646197037686 -0.999976253206104 0.78447116344273
+ name "my_robot"
+ controller ""
+ supervisor TRUE
+ lidarSlot [
+ Lidar {
+ name "hokuyo"
+ horizontalResolution 660
+ fieldOfView 3.14
+ numberOfLayers 1
+ maxRange 6
+ }
+ ]
+}
diff --git a/rust/provider/tiago_demo_package/nav2_webots_tiago/.gitignore b/rust/provider/tiago_demo_package/nav2_webots_tiago/.gitignore
new file mode 100644
index 0000000..84730b0
--- /dev/null
+++ b/rust/provider/tiago_demo_package/nav2_webots_tiago/.gitignore
@@ -0,0 +1 @@
+api/__init__.py
\ No newline at end of file
diff --git a/rust/provider/tiago_demo_package/nav2_webots_tiago/__init__.py b/rust/provider/tiago_demo_package/nav2_webots_tiago/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/rust/provider/tiago_demo_package/nav2_webots_tiago/config/amcl_params.yml b/rust/provider/tiago_demo_package/nav2_webots_tiago/config/amcl_params.yml
new file mode 100644
index 0000000..25386e4
--- /dev/null
+++ b/rust/provider/tiago_demo_package/nav2_webots_tiago/config/amcl_params.yml
@@ -0,0 +1,25 @@
+amcl:
+ ros__parameters:
+ use_sim_time: True
+ transform_tolerance: 0.3
+ tf_broadcast: true
+ scan_topic: /scanner
+ min_particles: 300
+ max_particles: 2000
+ odom_frame_id: "odom"
+ base_frame_id: "base_footprint"
+ global_frame_id: "map"
+ laser_max_range: -1.0
+ laser_min_range: -1.0
+ # Set to negative values to enable continuous pose publishing even when robot is stationary
+ # This causes AMCL to publish amcl_pose on every scan arrival
+ update_min_d: -1.0
+ update_min_a: -1.0
+ resample_interval: 1
+ set_initial_pose: true
+ initial_pose:
+ x: 0.0
+ y: 0.0
+ z: 0.0
+ yaw: 0.0
+
diff --git a/rust/provider/tiago_demo_package/nav2_webots_tiago/config/map.pgm b/rust/provider/tiago_demo_package/nav2_webots_tiago/config/map.pgm
new file mode 100644
index 0000000..3abf932
Binary files /dev/null and b/rust/provider/tiago_demo_package/nav2_webots_tiago/config/map.pgm differ
diff --git a/rust/provider/tiago_demo_package/nav2_webots_tiago/config/my_map.yml b/rust/provider/tiago_demo_package/nav2_webots_tiago/config/my_map.yml
new file mode 100644
index 0000000..35b753c
--- /dev/null
+++ b/rust/provider/tiago_demo_package/nav2_webots_tiago/config/my_map.yml
@@ -0,0 +1,7 @@
+image: map.pgm
+mode: trinary
+resolution: 0.05
+origin: [-12.2, -4.96, 0]
+negate: 0
+occupied_thresh: 0.65
+free_thresh: 0.25
diff --git a/rust/provider/tiago_demo_package/nav2_webots_tiago/config/my_navigate_w_replanning_and_recovery.xml b/rust/provider/tiago_demo_package/nav2_webots_tiago/config/my_navigate_w_replanning_and_recovery.xml
new file mode 100644
index 0000000..bf6fccb
--- /dev/null
+++ b/rust/provider/tiago_demo_package/nav2_webots_tiago/config/my_navigate_w_replanning_and_recovery.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/rust/provider/tiago_demo_package/nav2_webots_tiago/config/nav2_params.yml b/rust/provider/tiago_demo_package/nav2_webots_tiago/config/nav2_params.yml
new file mode 100644
index 0000000..086a4ed
--- /dev/null
+++ b/rust/provider/tiago_demo_package/nav2_webots_tiago/config/nav2_params.yml
@@ -0,0 +1,314 @@
+amcl:
+ ros__parameters:
+ use_sim_time: True
+ transform_tolerance: 0.3
+ tf_broadcast: true
+ scan_topic: /scanner
+ min_particles: 300
+ max_particles: 2000
+ odom_frame_id: "odom"
+ base_frame_id: "base_footprint"
+ global_frame_id: "map"
+ laser_max_range: -1.0
+ laser_min_range: -1.0
+ # Set to negative values to enable continuous pose publishing even when robot is stationary
+ # This causes AMCL to publish amcl_pose on every scan arrival
+ update_min_d: -1.0
+ update_min_a: -1.0
+ resample_interval: 1
+ set_initial_pose: true
+ initial_pose:
+ x: 0.0
+ y: 0.0
+ z: 0.0
+ yaw: 0.0
+map_server:
+ ros__parameters:
+ use_sim_time: True
+ # Overridden in launch by the "map" launch configuration or provided default value.
+ # To use in yaml, remove the default "map" value in the tb3_simulation_launch.py file & provide full path to map below.
+ yaml_filename: "/home/vuken/Robonix/robonix/capability/navigation2/config/my_map.yml"
+bt_navigator:
+ ros__parameters:
+ use_sim_time: True
+ global_frame: map
+ robot_base_frame: base_link
+ odom_topic: /odom
+ bt_loop_duration: 10
+ default_server_timeout: 20
+ # 'default_nav_through_poses_bt_xml' and 'default_nav_to_pose_bt_xml' are use defaults:
+ # nav2_bt_navigator/navigate_to_pose_w_replanning_and_recovery.xml
+ # nav2_bt_navigator/navigate_through_poses_w_replanning_and_recovery.xml
+ # They can be set here or via a RewrittenYaml remap from a parent launch file to Nav2.
+ plugin_lib_names:
+ - nav2_compute_path_to_pose_action_bt_node
+ - nav2_compute_path_through_poses_action_bt_node
+ - nav2_smooth_path_action_bt_node
+ - nav2_follow_path_action_bt_node
+ - nav2_spin_action_bt_node
+ - nav2_wait_action_bt_node
+ - nav2_assisted_teleop_action_bt_node
+ - nav2_back_up_action_bt_node
+ - nav2_drive_on_heading_bt_node
+ - nav2_clear_costmap_service_bt_node
+ - nav2_is_stuck_condition_bt_node
+ - nav2_goal_reached_condition_bt_node
+ - nav2_goal_updated_condition_bt_node
+ - nav2_globally_updated_goal_condition_bt_node
+ - nav2_is_path_valid_condition_bt_node
+ - nav2_initial_pose_received_condition_bt_node
+ - nav2_reinitialize_global_localization_service_bt_node
+ - nav2_rate_controller_bt_node
+ - nav2_distance_controller_bt_node
+ - nav2_speed_controller_bt_node
+ - nav2_truncate_path_action_bt_node
+ - nav2_truncate_path_local_action_bt_node
+ - nav2_goal_updater_node_bt_node
+ - nav2_recovery_node_bt_node
+ - nav2_pipeline_sequence_bt_node
+ - nav2_round_robin_node_bt_node
+ - nav2_transform_available_condition_bt_node
+ - nav2_time_expired_condition_bt_node
+ - nav2_path_expiring_timer_condition
+ - nav2_distance_traveled_condition_bt_node
+ - nav2_single_trigger_bt_node
+ - nav2_goal_updated_controller_bt_node
+ - nav2_is_battery_low_condition_bt_node
+ - nav2_navigate_through_poses_action_bt_node
+ - nav2_navigate_to_pose_action_bt_node
+ - nav2_remove_passed_goals_action_bt_node
+ - nav2_planner_selector_bt_node
+ - nav2_controller_selector_bt_node
+ - nav2_goal_checker_selector_bt_node
+ - nav2_controller_cancel_bt_node
+ - nav2_path_longer_on_approach_bt_node
+ - nav2_wait_cancel_bt_node
+ - nav2_spin_cancel_bt_node
+ - nav2_back_up_cancel_bt_node
+ - nav2_assisted_teleop_cancel_bt_node
+ - nav2_drive_on_heading_cancel_bt_node
+
+bt_navigator_navigate_through_poses_rclcpp_node:
+ ros__parameters:
+ use_sim_time: True
+
+bt_navigator_navigate_to_pose_rclcpp_node:
+ ros__parameters:
+ use_sim_time: True
+
+controller_server:
+ ros__parameters:
+ use_sim_time: True
+ controller_frequency: 20.0
+ min_x_velocity_threshold: 0.001
+ min_y_velocity_threshold: 0.5
+ min_theta_velocity_threshold: 0.001
+ failure_tolerance: 0.3
+ progress_checker_plugin: "progress_checker"
+ goal_checker_plugins: ["general_goal_checker"] # "precise_goal_checker"
+ controller_plugins: ["FollowPath"]
+
+ # Progress checker parameters
+ progress_checker:
+ plugin: "nav2_controller::SimpleProgressChecker"
+ required_movement_radius: 0.5
+ movement_time_allowance: 10.0
+ # Goal checker parameters
+ #precise_goal_checker:
+ # plugin: "nav2_controller::SimpleGoalChecker"
+ # xy_goal_tolerance: 0.25
+ # yaw_goal_tolerance: 0.25
+ # stateful: True
+ general_goal_checker:
+ stateful: True
+ plugin: "nav2_controller::SimpleGoalChecker"
+ xy_goal_tolerance: 0.25
+ yaw_goal_tolerance: 0.25
+ # DWB parameters
+ FollowPath:
+ plugin: "dwb_core::DWBLocalPlanner"
+ debug_trajectory_details: True
+ min_vel_x: 0.0
+ min_vel_y: 0.0
+ max_vel_x: 0.26
+ max_vel_y: 0.0
+ max_vel_theta: 1.0
+ min_speed_xy: 0.0
+ max_speed_xy: 0.26
+ min_speed_theta: 0.0
+ # Add high threshold velocity for turtlebot 3 issue.
+ # https://github.com/ROBOTIS-GIT/turtlebot3_simulations/issues/75
+ acc_lim_x: 2.5
+ acc_lim_y: 0.0
+ acc_lim_theta: 3.2
+ decel_lim_x: -2.5
+ decel_lim_y: 0.0
+ decel_lim_theta: -3.2
+ vx_samples: 20
+ vy_samples: 5
+ vtheta_samples: 20
+ sim_time: 1.7
+ linear_granularity: 0.05
+ angular_granularity: 0.025
+ transform_tolerance: 0.2
+ xy_goal_tolerance: 0.25
+ trans_stopped_velocity: 0.25
+ short_circuit_trajectory_evaluation: True
+ stateful: True
+ critics: ["RotateToGoal", "Oscillation", "BaseObstacle", "GoalAlign", "PathAlign", "PathDist", "GoalDist"]
+ BaseObstacle.scale: 0.02
+ PathAlign.scale: 32.0
+ PathAlign.forward_point_distance: 0.1
+ GoalAlign.scale: 24.0
+ GoalAlign.forward_point_distance: 0.1
+ PathDist.scale: 32.0
+ GoalDist.scale: 24.0
+ RotateToGoal.scale: 32.0
+ RotateToGoal.slowing_factor: 5.0
+ RotateToGoal.lookahead_time: -1.0
+
+local_costmap:
+ local_costmap:
+ ros__parameters:
+ update_frequency: 5.0
+ publish_frequency: 2.0
+ transform_tolerance: 0.3
+ global_frame: map # should be odom
+ robot_base_frame: base_link
+ use_sim_time: true
+ rolling_window: true
+ width: 5
+ height: 5
+ resolution: 0.05
+ robot_radius: 0.22
+ plugins: ["obstacle_layer", "inflation_layer"]
+ obstacle_layer:
+ plugin: "nav2_costmap_2d::ObstacleLayer"
+ enabled: True
+ observation_sources: scan
+ scan:
+ topic: /scanner
+ max_obstacle_height: 2.0
+ clearing: True
+ marking: True
+ data_type: "LaserScan"
+ raytrace_max_range: 3.0
+ raytrace_min_range: 0.0
+ obstacle_max_range: 2.5
+ obstacle_min_range: 0.0
+ inflation_layer:
+ plugin: "nav2_costmap_2d::InflationLayer"
+ cost_scaling_factor: 1.0
+ inflation_radius: 1.50
+ always_send_full_costmap: True
+
+global_costmap:
+ global_costmap:
+ ros__parameters:
+ update_frequency: 1.0
+ publish_frequency: 1.0
+ global_frame: map
+ robot_base_frame: base_link
+ use_sim_time: True
+ robot_radius: 0.22
+ resolution: 0.05
+ track_unknown_space: true
+ plugins: ["static_layer", "obstacle_layer", "inflation_layer"]
+ obstacle_layer:
+ plugin: "nav2_costmap_2d::ObstacleLayer"
+ enabled: True
+ observation_sources: scan
+ scan:
+ topic: /scanner
+ max_obstacle_height: 2.0
+ clearing: True
+ marking: True
+ data_type: "LaserScan"
+ raytrace_max_range: 3.0
+ raytrace_min_range: 0.0
+ obstacle_max_range: 2.5
+ obstacle_min_range: 0.0
+ static_layer:
+ plugin: "nav2_costmap_2d::StaticLayer"
+ map_subscribe_transient_local: True
+ inflation_layer:
+ plugin: "nav2_costmap_2d::InflationLayer"
+ cost_scaling_factor: 3.0
+ inflation_radius: 0.55
+ always_send_full_costmap: True
+
+planner_server:
+ ros__parameters:
+ expected_planner_frequency: 20.0
+ use_sim_time: True
+ planner_plugins: ["GridBased"]
+ GridBased:
+ plugin: "nav2_navfn_planner/NavfnPlanner"
+ tolerance: 0.5
+ use_astar: false
+ allow_unknown: true
+
+smoother_server:
+ ros__parameters:
+ use_sim_time: True
+ smoother_plugins: ["simple_smoother"]
+ simple_smoother:
+ plugin: "nav2_smoother::SimpleSmoother"
+ tolerance: 1.0e-10
+ max_its: 1000
+ do_refinement: True
+
+behavior_server:
+ ros__parameters:
+ costmap_topic: local_costmap/costmap_raw
+ footprint_topic: local_costmap/published_footprint
+ cycle_frequency: 10.0
+ behavior_plugins: ["spin", "backup", "drive_on_heading", "assisted_teleop", "wait"]
+ spin:
+ plugin: "nav2_behaviors/Spin"
+ backup:
+ plugin: "nav2_behaviors/BackUp"
+ drive_on_heading:
+ plugin: "nav2_behaviors/DriveOnHeading"
+ wait:
+ plugin: "nav2_behaviors/Wait"
+ assisted_teleop:
+ plugin: "nav2_behaviors/AssistedTeleop"
+ global_frame: odom
+ robot_base_frame: base_link
+ transform_tolerance: 0.1
+ use_sim_time: true
+ simulate_ahead_time: 2.0
+ max_rotational_vel: 1.0
+ min_rotational_vel: 0.4
+ rotational_acc_lim: 3.2
+
+robot_state_publisher:
+ ros__parameters:
+ use_sim_time: True
+
+waypoint_follower:
+ ros__parameters:
+ use_sim_time: True
+ loop_rate: 20
+ stop_on_failure: false
+ waypoint_task_executor_plugin: "wait_at_waypoint"
+ wait_at_waypoint:
+ plugin: "nav2_waypoint_follower::WaitAtWaypoint"
+ enabled: True
+ waypoint_pause_duration: 200
+
+velocity_smoother:
+ ros__parameters:
+ use_sim_time: True
+ smoothing_frequency: 20.0
+ scale_velocities: False
+ feedback: "OPEN_LOOP"
+ max_velocity: [0.26, 0.0, 1.0]
+ min_velocity: [-0.26, 0.0, -1.0]
+ max_accel: [2.5, 0.0, 3.2]
+ max_decel: [-2.5, 0.0, -3.2]
+ odom_topic: "odom"
+ odom_duration: 0.1
+ deadband_velocity: [0.0, 0.0, 0.0]
+ velocity_timeout: 1.0
diff --git a/rust/provider/tiago_demo_package/nav2_webots_tiago/run.sh b/rust/provider/tiago_demo_package/nav2_webots_tiago/run.sh
new file mode 100755
index 0000000..906250e
--- /dev/null
+++ b/rust/provider/tiago_demo_package/nav2_webots_tiago/run.sh
@@ -0,0 +1,3 @@
+source /opt/ros/humble/setup.bash
+
+ros2 launch nav2_bringup bringup_launch.py map:=config/my_map.yml use_sim_time:=true params_file:=config/nav2_params.yml
diff --git a/rust/provider/tiago_demo_package/rbnx/build.sh b/rust/provider/tiago_demo_package/rbnx/build.sh
new file mode 100755
index 0000000..168e236
--- /dev/null
+++ b/rust/provider/tiago_demo_package/rbnx/build.sh
@@ -0,0 +1,156 @@
+#!/bin/bash
+# SPDX-License-Identifier: MulanPSL-2.0
+# Build Tiago Demo Package Script
+#
+# Build script for tiago_demo_package
+# This script is executed by 'rbnx deploy build' command
+# It should compile, install dependencies, or perform any necessary build steps
+
+set -e # Exit on error
+
+echo "Building tiago_demo_package..."
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PACKAGE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
+cd "$PACKAGE_DIR"
+
+# Fix conda environment issues
+if [ -n "$CONDA_PREFIX" ]; then
+ export LD_LIBRARY_PATH="/usr/lib/x86_64-linux-gnu:/lib/x86_64-linux-gnu:${LD_LIBRARY_PATH}"
+ export LD_LIBRARY_PATH=$(echo "$LD_LIBRARY_PATH" | tr ':' '\n' | grep -v "$CONDA_PREFIX/lib" | tr '\n' ':' | sed 's/:$//')
+ unset CONDA_DEFAULT_ENV
+ unset CONDA_PREFIX
+ unset CONDA_PROMPT_MODIFIER
+ unset CONDA_PYTHON_EXE
+ unset CONDA_SHLVL
+ export PATH=$(echo $PATH | tr ':' '\n' | grep -v conda | tr '\n' ':' | sed 's/:$//')
+fi
+
+# Use system Python explicitly
+export PYTHON3_EXECUTABLE=/usr/bin/python3
+export PYTHON_EXECUTABLE=/usr/bin/python3
+
+# Install required ROS2 dependencies
+echo "Installing ROS2 dependencies..."
+DEPS=(
+ "ros-humble-controller-interface"
+ "ros-humble-hardware-interface"
+ "ros-humble-realtime-tools"
+ "ros-humble-rclcpp-lifecycle"
+ "ros-humble-nav-msgs"
+ "ros-humble-rcpputils"
+ "ros-humble-tf2"
+ "ros-humble-tf2-msgs"
+ "ros-humble-pluginlib"
+ "ros-humble-controller-manager"
+ "ros-humble-joint-state-broadcaster"
+ "ros-humble-robot-state-publisher"
+ "ros-humble-tf2-ros"
+ "ros-humble-diff-drive-controller"
+ "ros-humble-webots-ros2-driver"
+ "ros-humble-webots-ros2-control"
+ "ros-humble-webots-ros2"
+ "ros-humble-nav2-bringup"
+ "ros-humble-nav2-map-server"
+ "ros-humble-nav2-amcl"
+ "ros-humble-nav2-common"
+ "ros-humble-nav2-controller"
+ "ros-humble-nav2-core"
+ "ros-humble-nav2-costmap-2d"
+ "ros-humble-nav2-msgs"
+ "ros-humble-nav2-navfn-planner"
+ "ros-humble-nav2-planner"
+ "ros-humble-nav2-behaviors"
+ "ros-humble-nav2-util"
+ "ros-humble-nav2-voxel-grid"
+ "ros-humble-nav2-behavior-tree"
+ "ros-humble-nav2-bt-navigator"
+ "ros-humble-nav2-lifecycle-manager"
+ "ros-humble-nav2-simple-commander"
+ "ros-humble-rviz2"
+ "ros-humble-image-transport"
+ "ros-humble-image-transport-plugins"
+ "ros-humble-sensor-msgs"
+ "ros-humble-geometry-msgs"
+ "ros-humble-std-msgs"
+)
+
+# Check and install missing dependencies
+MISSING_DEPS=()
+for dep in "${DEPS[@]}"; do
+ if ! dpkg -l | grep -q "^ii.*${dep}"; then
+ MISSING_DEPS+=("${dep}")
+ fi
+done
+
+if [ ${#MISSING_DEPS[@]} -gt 0 ]; then
+ echo "Installing missing dependencies: ${MISSING_DEPS[*]}"
+ # Update package lists and retry on failure
+ sudo apt-get update -qq || sudo apt-get update
+ # Try to install with retry logic
+ MAX_RETRIES=3
+ RETRY_COUNT=0
+ while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
+ if sudo apt-get install -y "${MISSING_DEPS[@]}" 2>&1; then
+ break
+ fi
+ RETRY_COUNT=$((RETRY_COUNT + 1))
+ if [ $RETRY_COUNT -lt $MAX_RETRIES ]; then
+ echo "Installation attempt $RETRY_COUNT failed, retrying..."
+ sleep 2
+ sudo apt-get update -qq || sudo apt-get update
+ else
+ echo "Warning: Some dependencies could not be installed after $MAX_RETRIES attempts."
+ echo "Attempting to continue with build..."
+ fi
+ done
+fi
+
+# Verify critical dependencies are installed
+CRITICAL_DEPS=("ros-humble-controller-interface" "ros-humble-hardware-interface" "ros-humble-controller-manager" "ros-humble-webots-ros2-driver")
+MISSING_CRITICAL=()
+for dep in "${CRITICAL_DEPS[@]}"; do
+ if ! dpkg -l | grep -q "^ii.*${dep}"; then
+ MISSING_CRITICAL+=("${dep}")
+ fi
+done
+
+if [ ${#MISSING_CRITICAL[@]} -gt 0 ]; then
+ echo "Error: Critical dependencies are missing: ${MISSING_CRITICAL[*]}"
+ echo "Please install them manually: apt-get install -y ${MISSING_CRITICAL[*]}"
+ exit 1
+fi
+
+# Source ROS2 environment (must be done after installing packages)
+if [ -f /opt/ros/humble/setup.bash ]; then
+ source /opt/ros/humble/setup.bash
+else
+ echo "Error: ROS2 Humble setup.bash not found at /opt/ros/humble/setup.bash"
+ exit 1
+fi
+
+# Clean previous build if it exists (to ensure fresh build with new dependencies)
+if [ -d "build" ] || [ -d "install" ]; then
+ echo "Cleaning previous build artifacts..."
+ rm -rf build install log
+fi
+
+# Build packages using colcon
+echo "Building tiago_demo_package with colcon..."
+if ! command -v colcon > /dev/null 2>&1; then
+ echo "Error: colcon not found. Please install colcon-common-extensions."
+ exit 1
+fi
+
+# Ensure ROS2 environment is sourced before building
+source /opt/ros/humble/setup.bash
+
+# Build with proper environment
+colcon build \
+ --symlink-install \
+ --cmake-args \
+ -DPYTHON3_EXECUTABLE=/usr/bin/python3 \
+ -DCMAKE_PREFIX_PATH=/opt/ros/humble
+
+echo "Package built successfully!"
+echo "Build completed successfully!"
diff --git a/rust/provider/tiago_demo_package/rbnx/start_base_navigate.sh b/rust/provider/tiago_demo_package/rbnx/start_base_navigate.sh
new file mode 100755
index 0000000..1b70b5e
--- /dev/null
+++ b/rust/provider/tiago_demo_package/rbnx/start_base_navigate.sh
@@ -0,0 +1,40 @@
+#!/bin/bash
+# SPDX-License-Identifier: MulanPSL-2.0
+# Start Base Navigate Primitive Script
+#
+# Start script for prm::base.navigate (navigate to target pose)
+# This script verifies that Nav2 is running and the goal topic is available.
+# It also starts a simple node to publish navigation status.
+
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PACKAGE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
+cd "$PACKAGE_DIR"
+
+# Source ROS2 setup if available
+if [ -f /opt/ros/humble/setup.bash ]; then
+ source /opt/ros/humble/setup.bash
+fi
+
+# Source local setup if available
+if [ -f "install/setup.bash" ]; then
+ source install/setup.bash 2>/dev/null || true
+fi
+
+# Wait for Nav2 action server to be available (up to 30 seconds)
+echo "Waiting for Nav2 navigation action server..."
+TIMEOUT=30
+ELAPSED=0
+while [ $ELAPSED -lt $TIMEOUT ]; do
+ if ros2 action list 2>/dev/null | grep -q "navigate_to_pose"; then
+ echo "Nav2 navigation action server is available!"
+ echo "Goal can be published to /goal_pose"
+ exit 0
+ fi
+ sleep 1
+ ELAPSED=$((ELAPSED + 1))
+done
+
+echo "Warning: Nav2 navigation action server not found. Make sure Nav2 is running"
+exit 1
diff --git a/rust/provider/tiago_demo_package/rbnx/start_base_pose_amcl.sh b/rust/provider/tiago_demo_package/rbnx/start_base_pose_amcl.sh
new file mode 100755
index 0000000..269e52d
--- /dev/null
+++ b/rust/provider/tiago_demo_package/rbnx/start_base_pose_amcl.sh
@@ -0,0 +1,41 @@
+#!/bin/bash
+# SPDX-License-Identifier: MulanPSL-2.0
+# Start Base Pose AMCL Primitive Script
+#
+# Start script for prm::base.pose.cov (robot pose in map frame directly from AMCL)
+# This script verifies that AMCL is running and the /amcl_pose topic is available.
+# No converter needed - uses PoseWithCovarianceStamped directly.
+
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PACKAGE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
+cd "$PACKAGE_DIR"
+
+# Source ROS2 setup if available
+if [ -f /opt/ros/humble/setup.bash ]; then
+ source /opt/ros/humble/setup.bash
+fi
+
+# Source local setup if available
+if [ -f "install/setup.bash" ]; then
+ source install/setup.bash 2>/dev/null || true
+fi
+
+# Wait for the AMCL pose topic to be available (up to 30 seconds)
+echo "Waiting for AMCL pose topic /amcl_pose..."
+TIMEOUT=30
+ELAPSED=0
+while [ $ELAPSED -lt $TIMEOUT ]; do
+ if ros2 topic list 2>/dev/null | grep -q "^/amcl_pose$"; then
+ echo "AMCL pose topic is available!"
+ exit 0
+ fi
+ sleep 1
+ ELAPSED=$((ELAPSED + 1))
+done
+
+if [ $ELAPSED -ge $TIMEOUT ]; then
+ echo "Warning: AMCL pose topic not found. Make sure AMCL is running (e.g., via nav2 launch)"
+ exit 1
+fi
diff --git a/rust/provider/tiago_demo_package/rbnx/start_camera_depth.sh b/rust/provider/tiago_demo_package/rbnx/start_camera_depth.sh
new file mode 100755
index 0000000..4964ce5
--- /dev/null
+++ b/rust/provider/tiago_demo_package/rbnx/start_camera_depth.sh
@@ -0,0 +1,39 @@
+#!/bin/bash
+# SPDX-License-Identifier: MulanPSL-2.0
+# Start Depth Camera Primitive Script
+#
+# Start script for prm::camera.depth (depth camera)
+# Note: The depth camera is provided by webots_ros2_driver when webots is running.
+# This script verifies the topic is available or waits for it.
+
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PACKAGE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
+cd "$PACKAGE_DIR"
+
+# Source ROS2 setup if available
+if [ -f /opt/ros/humble/setup.bash ]; then
+ source /opt/ros/humble/setup.bash
+fi
+
+# Source local setup if available
+if [ -f "install/setup.bash" ]; then
+ source install/setup.bash 2>/dev/null || true
+fi
+
+# Wait for the depth camera topic to be available (up to 30 seconds)
+echo "Waiting for depth camera topic /head_front_camera/depth_registered/image_raw..."
+TIMEOUT=30
+ELAPSED=0
+while [ $ELAPSED -lt $TIMEOUT ]; do
+ if ros2 topic list 2>/dev/null | grep -q "/head_front_camera/depth_registered/image_raw"; then
+ echo "Depth camera topic is available!"
+ exit 0
+ fi
+ sleep 1
+ ELAPSED=$((ELAPSED + 1))
+done
+
+echo "Warning: Depth camera topic not found. Make sure webots is running with robot_launch.py"
+exit 1
diff --git a/rust/provider/tiago_demo_package/rbnx/start_camera_rgb.sh b/rust/provider/tiago_demo_package/rbnx/start_camera_rgb.sh
new file mode 100755
index 0000000..6e39781
--- /dev/null
+++ b/rust/provider/tiago_demo_package/rbnx/start_camera_rgb.sh
@@ -0,0 +1,39 @@
+#!/bin/bash
+# SPDX-License-Identifier: MulanPSL-2.0
+# Start RGB Camera Primitive Script
+#
+# Start script for prm::camera.rgb (RGB camera)
+# Note: The camera is provided by webots_ros2_driver when webots is running.
+# This script verifies the topic is available or waits for it.
+
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PACKAGE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
+cd "$PACKAGE_DIR"
+
+# Source ROS2 setup if available
+if [ -f /opt/ros/humble/setup.bash ]; then
+ source /opt/ros/humble/setup.bash
+fi
+
+# Source local setup if available
+if [ -f "install/setup.bash" ]; then
+ source install/setup.bash 2>/dev/null || true
+fi
+
+# Wait for the camera topic to be available (up to 30 seconds)
+echo "Waiting for RGB camera topic /head_front_camera/rgb/image_raw..."
+TIMEOUT=30
+ELAPSED=0
+while [ $ELAPSED -lt $TIMEOUT ]; do
+ if ros2 topic list 2>/dev/null | grep -q "/head_front_camera/rgb/image_raw"; then
+ echo "RGB camera topic is available!"
+ exit 0
+ fi
+ sleep 1
+ ELAPSED=$((ELAPSED + 1))
+done
+
+echo "Warning: RGB camera topic not found. Make sure webots is running with robot_launch.py"
+exit 1
diff --git a/rust/provider/tiago_demo_package/rbnx/stop_base_navigate.sh b/rust/provider/tiago_demo_package/rbnx/stop_base_navigate.sh
new file mode 100755
index 0000000..79e06c7
--- /dev/null
+++ b/rust/provider/tiago_demo_package/rbnx/stop_base_navigate.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+# SPDX-License-Identifier: MulanPSL-2.0
+# Stop Base Navigate Primitive Script
+#
+# Stop script for prm::base.navigate (navigate to target pose)
+# Note: Nav2 is typically managed by launch files, so this is a no-op.
+
+set -e
+
+echo "Base navigate primitive stop requested (no-op: Nav2 is managed by launch files)"
+exit 0
diff --git a/rust/provider/tiago_demo_package/rbnx/stop_base_pose_amcl.sh b/rust/provider/tiago_demo_package/rbnx/stop_base_pose_amcl.sh
new file mode 100755
index 0000000..b03f80d
--- /dev/null
+++ b/rust/provider/tiago_demo_package/rbnx/stop_base_pose_amcl.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+# SPDX-License-Identifier: MulanPSL-2.0
+# Stop Base Pose AMCL Primitive Script
+#
+# Stop script for prm::base.pose.cov
+# No converter process to stop - just verify topic is available
+
+# Nothing to stop for this primitive (AMCL is managed externally)
+echo "prm::base.pose.cov primitive stopped (AMCL topic /amcl_pose should remain available)"
diff --git a/rust/provider/tiago_demo_package/rbnx/stop_camera_depth.sh b/rust/provider/tiago_demo_package/rbnx/stop_camera_depth.sh
new file mode 100755
index 0000000..8dbd40a
--- /dev/null
+++ b/rust/provider/tiago_demo_package/rbnx/stop_camera_depth.sh
@@ -0,0 +1,12 @@
+#!/bin/bash
+# SPDX-License-Identifier: MulanPSL-2.0
+# Stop Depth Camera Primitive Script
+#
+# Stop script for prm::camera.depth (depth camera)
+# Note: The depth camera is provided by webots_ros2_driver, so this is a no-op.
+# The topic will stop when webots is stopped.
+
+set -e
+
+echo "Depth camera primitive stop requested (no-op: camera is managed by webots_ros2_driver)"
+exit 0
diff --git a/rust/provider/tiago_demo_package/rbnx/stop_camera_rgb.sh b/rust/provider/tiago_demo_package/rbnx/stop_camera_rgb.sh
new file mode 100755
index 0000000..deea4d5
--- /dev/null
+++ b/rust/provider/tiago_demo_package/rbnx/stop_camera_rgb.sh
@@ -0,0 +1,12 @@
+#!/bin/bash
+# SPDX-License-Identifier: MulanPSL-2.0
+# Stop RGB Camera Primitive Script
+#
+# Stop script for prm::camera.rgb (RGB camera)
+# Note: The camera is provided by webots_ros2_driver, so this is a no-op.
+# The topic will stop when webots is stopped.
+
+set -e
+
+echo "RGB camera primitive stop requested (no-op: camera is managed by webots_ros2_driver)"
+exit 0
diff --git a/rust/provider/tiago_demo_package/rbnx_manifest.yaml b/rust/provider/tiago_demo_package/rbnx_manifest.yaml
new file mode 100644
index 0000000..8de64fd
--- /dev/null
+++ b/rust/provider/tiago_demo_package/rbnx_manifest.yaml
@@ -0,0 +1,59 @@
+# Robonix Package Manifest
+# This file defines the primitives, services, and skills provided by this package
+# Format version: 2.0 (EAIOS Architecture)
+#
+# Note:
+# - Primitives and services must conform to standard specifications
+# - Skills are flexible and user-defined
+# - JSON fields (input_schema, output_schema, metadata, start_args, status) must be valid JSON strings
+
+package:
+ name: tiago_demo_package
+ version: 0.0.1
+ description: Webots Tiago robot package providing camera, depth, pose, and navigation primitives
+ maintainer: root
+ maintainer_email: demo@demo.demo
+ license: MulanPSL-2.0
+ build_script: rbnx/build.sh
+
+# Primitives provided by this package
+# Primitives provide standardized hardware capability mapping
+primitives:
+ # Front camera RGB
+ - name: prm::camera.rgb
+ # Spec defines: OUTPUT: {"image": "sensor_msgs/msg/Image"}
+ input_schema: '{}'
+ output_schema: '{"image":"/head_front_camera/rgb/image_raw"}'
+ metadata: '{"camera":"front","robot":"tiago","simulator":"webots"}'
+ version: 0.0.1
+ start_script: rbnx/start_camera_rgb.sh
+ stop_script: rbnx/stop_camera_rgb.sh
+
+ # Front camera depth
+ - name: prm::camera.depth
+ # Spec defines: OUTPUT: {"depth": "sensor_msgs/msg/Image"}
+ input_schema: '{}'
+ output_schema: '{"depth":"/head_front_camera/depth_registered/image_raw"}'
+ metadata: '{"camera":"front_depth","robot":"tiago","simulator":"webots"}'
+ version: 0.0.1
+ start_script: rbnx/start_camera_depth.sh
+ stop_script: rbnx/stop_camera_depth.sh
+
+ # Robot pose in map frame directly from AMCL - PoseWithCovarianceStamped version
+ - name: prm::base.pose.cov
+ # Spec defines: OUTPUT: {"pose": "geometry_msgs/msg/PoseWithCovarianceStamped"}
+ input_schema: '{}'
+ output_schema: '{"pose":"/amcl_pose"}'
+ metadata: '{"robot":"tiago","localization":"amcl","simulator":"webots","pose_type":"PoseWithCovarianceStamped"}'
+ version: 0.0.1
+ start_script: rbnx/start_base_pose_amcl.sh
+ stop_script: rbnx/stop_base_pose_amcl.sh
+
+ # Navigate to target pose (skill detects arrival via pose: stationary + within delta)
+ - name: prm::base.navigate
+ input_schema: '{"goal":"/goal_pose"}'
+ output_schema: '{}'
+ metadata: '{"robot":"tiago","navigation":"nav2","simulator":"webots","action_server":"/navigate_to_pose"}'
+ version: 0.0.1
+ start_script: rbnx/start_base_navigate.sh
+ stop_script: rbnx/stop_base_navigate.sh
diff --git a/rust/provider/tiago_demo_package/run.sh b/rust/provider/tiago_demo_package/run.sh
new file mode 100755
index 0000000..7457ccb
--- /dev/null
+++ b/rust/provider/tiago_demo_package/run.sh
@@ -0,0 +1,5 @@
+#! /bin/bash
+# setup for ros2
+source install/setup.bash
+# run
+ros2 launch eaios_webots robot_launch.py
diff --git a/rust/provider/tiago_demo_package/rviz.config.rviz b/rust/provider/tiago_demo_package/rviz.config.rviz
new file mode 100644
index 0000000..cea47f3
--- /dev/null
+++ b/rust/provider/tiago_demo_package/rviz.config.rviz
@@ -0,0 +1,535 @@
+Panels:
+ - Class: rviz_common/Displays
+ Help Height: 0
+ Name: Displays
+ Property Tree Widget:
+ Expanded:
+ - /Global Options1
+ - /Status1
+ - /Map2/Topic1
+ - /LaserScan1/Topic1
+ - /ParticleCloud1/Topic1
+ - /Pose1
+ - /Odometry1
+ - /Odometry1/Shape1
+ Splitter Ratio: 0.5106382966041565
+ Tree Height: 926
+ - Class: rviz_common/Selection
+ Name: Selection
+ - Class: rviz_common/Tool Properties
+ Expanded:
+ - /2D Goal Pose1
+ - /Publish Point1
+ Name: Tool Properties
+ Splitter Ratio: 0.5886790156364441
+ - Class: rviz_common/Views
+ Expanded:
+ - /Current View1
+ Name: Views
+ Splitter Ratio: 0.5
+ - Class: rviz_common/Time
+ Experimental: false
+ Name: Time
+ SyncMode: 0
+ SyncSource: PointCloud2
+ - Class: nav2_rviz_plugins/Navigation 2
+ Name: Navigation 2
+Visualization Manager:
+ Class: ""
+ Displays:
+ - Alpha: 0.5
+ Cell Size: 1
+ Class: rviz_default_plugins/Grid
+ Color: 160; 160; 164
+ Enabled: true
+ Line Style:
+ Line Width: 0.029999999329447746
+ Value: Lines
+ Name: Grid
+ Normal Cell Count: 0
+ Offset:
+ X: 0
+ Y: 0
+ Z: 0
+ Plane: XY
+ Plane Cell Count: 10
+ Reference Frame:
+ Value: true
+ - Class: rviz_default_plugins/TF
+ Enabled: true
+ Frame Timeout: 15
+ Frames:
+ All Enabled: true
+ Astra:
+ Value: true
+ Astra depth:
+ Value: true
+ Astra rgb:
+ Value: true
+ Torso:
+ Value: true
+ accelerometer:
+ Value: true
+ base_cover_link:
+ Value: true
+ base_footprint:
+ Value: true
+ base_link:
+ Value: true
+ base_sonar_01_link:
+ Value: true
+ base_sonar_02_link:
+ Value: true
+ base_sonar_03_link:
+ Value: true
+ caster_back_left_1_link:
+ Value: true
+ caster_back_left_2_link:
+ Value: true
+ caster_back_right_1_link:
+ Value: true
+ caster_back_right_2_link:
+ Value: true
+ caster_front_left_1_link:
+ Value: true
+ caster_front_left_2_link:
+ Value: true
+ caster_front_right_1_link:
+ Value: true
+ caster_front_right_2_link:
+ Value: true
+ gyro:
+ Value: true
+ head_1_link:
+ Value: true
+ head_2_link:
+ Value: true
+ hokuyo:
+ Value: true
+ inertial unit:
+ Value: true
+ map:
+ Value: true
+ odom:
+ Value: true
+ torso_lift_link:
+ Value: true
+ wheel_left_link:
+ Value: true
+ wheel_right_link:
+ Value: true
+ Marker Scale: 1
+ Name: TF
+ Show Arrows: true
+ Show Axes: true
+ Show Names: false
+ Tree:
+ map:
+ odom:
+ base_link:
+ Torso:
+ torso_lift_link:
+ head_1_link:
+ head_2_link:
+ Astra:
+ Astra depth:
+ {}
+ Astra rgb:
+ {}
+ accelerometer:
+ {}
+ base_cover_link:
+ {}
+ base_footprint:
+ {}
+ base_sonar_01_link:
+ {}
+ base_sonar_02_link:
+ {}
+ base_sonar_03_link:
+ {}
+ caster_back_left_1_link:
+ caster_back_left_2_link:
+ {}
+ caster_back_right_1_link:
+ caster_back_right_2_link:
+ {}
+ caster_front_left_1_link:
+ caster_front_left_2_link:
+ {}
+ caster_front_right_1_link:
+ caster_front_right_2_link:
+ {}
+ gyro:
+ {}
+ hokuyo:
+ {}
+ inertial unit:
+ {}
+ wheel_left_link:
+ {}
+ wheel_right_link:
+ {}
+ Update Interval: 0
+ Value: true
+ - Alpha: 0.699999988079071
+ Class: rviz_default_plugins/Map
+ Color Scheme: map
+ Draw Behind: false
+ Enabled: true
+ Name: Map
+ Topic:
+ Depth: 5
+ Durability Policy: Volatile
+ Filter size: 10
+ History Policy: Keep Last
+ Reliability Policy: Reliable
+ Value: /map
+ Update Topic:
+ Depth: 5
+ Durability Policy: Volatile
+ History Policy: Keep Last
+ Reliability Policy: Reliable
+ Value: /map_updates
+ Use Timestamp: false
+ Value: true
+ - Alpha: 0.30000001192092896
+ Class: rviz_default_plugins/Map
+ Color Scheme: map
+ Draw Behind: false
+ Enabled: true
+ Name: Map
+ Topic:
+ Depth: 5
+ Durability Policy: Volatile
+ Filter size: 10
+ History Policy: Keep Last
+ Reliability Policy: Best Effort
+ Value: /global_costmap/costmap
+ Update Topic:
+ Depth: 5
+ Durability Policy: Volatile
+ History Policy: Keep Last
+ Reliability Policy: Reliable
+ Value: /global_costmap/costmap_updates
+ Use Timestamp: false
+ Value: true
+ - Alpha: 0.699999988079071
+ Class: rviz_default_plugins/Map
+ Color Scheme: costmap
+ Draw Behind: true
+ Enabled: true
+ Name: Map
+ Topic:
+ Depth: 5
+ Durability Policy: Volatile
+ Filter size: 10
+ History Policy: Keep Last
+ Reliability Policy: Reliable
+ Value: /local_costmap/costmap
+ Update Topic:
+ Depth: 5
+ Durability Policy: Volatile
+ History Policy: Keep Last
+ Reliability Policy: Reliable
+ Value: /local_costmap/costmap_updates
+ Use Timestamp: false
+ Value: true
+ - Alpha: 1
+ Buffer Length: 1
+ Class: rviz_default_plugins/Path
+ Color: 25; 255; 0
+ Enabled: true
+ Head Diameter: 0.30000001192092896
+ Head Length: 0.20000000298023224
+ Length: 0.30000001192092896
+ Line Style: Lines
+ Line Width: 0.029999999329447746
+ Name: Path
+ Offset:
+ X: 0
+ Y: 0
+ Z: 0
+ Pose Color: 255; 85; 255
+ Pose Style: None
+ Radius: 0.029999999329447746
+ Shaft Diameter: 0.10000000149011612
+ Shaft Length: 0.10000000149011612
+ Topic:
+ Depth: 5
+ Durability Policy: Volatile
+ Filter size: 10
+ History Policy: Keep Last
+ Reliability Policy: Reliable
+ Value: /plan
+ Value: true
+ - Alpha: 1
+ Autocompute Intensity Bounds: true
+ Autocompute Value Bounds:
+ Max Value: 10
+ Min Value: -10
+ Value: true
+ Axis: Z
+ Channel Name: intensity
+ Class: rviz_default_plugins/LaserScan
+ Color: 255; 255; 255
+ Color Transformer: Intensity
+ Decay Time: 1
+ Enabled: true
+ Invert Rainbow: false
+ Max Color: 255; 255; 255
+ Max Intensity: 4096
+ Min Color: 0; 0; 0
+ Min Intensity: 0
+ Name: LaserScan
+ Position Transformer: XYZ
+ Selectable: true
+ Size (Pixels): 3
+ Size (m): 0.20000000298023224
+ Style: Flat Squares
+ Topic:
+ Depth: 5
+ Durability Policy: Volatile
+ Filter size: 10
+ History Policy: Keep Last
+ Reliability Policy: Best Effort
+ Value: /scanner/scan
+ Use Fixed Frame: true
+ Use rainbow: true
+ Value: true
+ - Alpha: 1
+ Autocompute Intensity Bounds: true
+ Autocompute Value Bounds:
+ Max Value: 10
+ Min Value: -10
+ Value: true
+ Axis: Z
+ Channel Name: intensity
+ Class: rviz_default_plugins/PointCloud2
+ Color: 255; 255; 255
+ Color Transformer: Intensity
+ Decay Time: 0
+ Enabled: true
+ Invert Rainbow: false
+ Max Color: 255; 255; 255
+ Max Intensity: 255
+ Min Color: 0; 0; 0
+ Min Intensity: 16
+ Name: PointCloud2
+ Position Transformer: XYZ
+ Selectable: true
+ Size (Pixels): 3
+ Size (m): 0.009999999776482582
+ Style: Flat Squares
+ Topic:
+ Depth: 5
+ Durability Policy: Volatile
+ Filter size: 10
+ History Policy: Keep Last
+ Reliability Policy: Reliable
+ Value: /scanner/point_cloud
+ Use Fixed Frame: true
+ Use rainbow: true
+ Value: true
+ - Alpha: 1
+ Class: nav2_rviz_plugins/ParticleCloud
+ Color: 255; 25; 0
+ Enabled: true
+ Max Arrow Length: 0.30000001192092896
+ Min Arrow Length: 0.019999999552965164
+ Name: ParticleCloud
+ Shape: Arrow (Flat)
+ Topic:
+ Depth: 5
+ Durability Policy: Volatile
+ Filter size: 10
+ History Policy: Keep Last
+ Reliability Policy: Best Effort
+ Value: /particle_cloud
+ Value: true
+ - Alpha: 1
+ Class: rviz_default_plugins/Polygon
+ Color: 25; 255; 0
+ Enabled: true
+ Name: Polygon
+ Topic:
+ Depth: 5
+ Durability Policy: Volatile
+ Filter size: 10
+ History Policy: Keep Last
+ Reliability Policy: Reliable
+ Value: /local_costmap/published_footprint
+ Value: true
+ - Alpha: 0.5
+ Buffer Length: 1
+ Class: rviz_default_plugins/Path
+ Color: 25; 255; 0
+ Enabled: true
+ Head Diameter: 0.30000001192092896
+ Head Length: 0.20000000298023224
+ Length: 0.30000001192092896
+ Line Style: Lines
+ Line Width: 0.029999999329447746
+ Name: Path
+ Offset:
+ X: 0
+ Y: 0
+ Z: 0
+ Pose Color: 255; 85; 255
+ Pose Style: Arrows
+ Radius: 0.029999999329447746
+ Shaft Diameter: 0.10000000149011612
+ Shaft Length: 0.10000000149011612
+ Topic:
+ Depth: 5
+ Durability Policy: Volatile
+ Filter size: 10
+ History Policy: Keep Last
+ Reliability Policy: Reliable
+ Value: /local_plan
+ Value: true
+ - Alpha: 1
+ Axes Length: 1
+ Axes Radius: 0.10000000149011612
+ Class: rviz_default_plugins/Pose
+ Color: 255; 251; 128
+ Enabled: true
+ Head Length: 0.30000001192092896
+ Head Radius: 0.10000000149011612
+ Name: Pose
+ Shaft Length: 0.5
+ Shaft Radius: 0.05000000074505806
+ Shape: Arrow
+ Topic:
+ Depth: 5
+ Durability Policy: Volatile
+ Filter size: 10
+ History Policy: Keep Last
+ Reliability Policy: Reliable
+ Value: /goal_pose
+ Value: true
+ - Angle Tolerance: 0.10000000149011612
+ Class: rviz_default_plugins/Odometry
+ Covariance:
+ Orientation:
+ Alpha: 0.5
+ Color: 255; 255; 127
+ Color Style: Unique
+ Frame: Local
+ Offset: 1
+ Scale: 1
+ Value: true
+ Position:
+ Alpha: 0.30000001192092896
+ Color: 204; 51; 204
+ Scale: 1
+ Value: true
+ Value: true
+ Enabled: true
+ Keep: 42
+ Name: Odometry
+ Position Tolerance: 0.10000000149011612
+ Shape:
+ Alpha: 0.30000001192092896
+ Axes Length: 1
+ Axes Radius: 0.10000000149011612
+ Color: 255; 25; 0
+ Head Length: 0.30000001192092896
+ Head Radius: 0.10000000149011612
+ Shaft Length: 0.5
+ Shaft Radius: 0.05000000074505806
+ Value: Arrow
+ Topic:
+ Depth: 5
+ Durability Policy: Volatile
+ Filter size: 10
+ History Policy: Keep Last
+ Reliability Policy: Reliable
+ Value: /odom
+ Value: true
+ Enabled: true
+ Global Options:
+ Background Color: 48; 48; 48
+ Fixed Frame: map
+ Frame Rate: 30
+ Name: root
+ Tools:
+ - Class: rviz_default_plugins/Interact
+ Hide Inactive Objects: true
+ - Class: rviz_default_plugins/MoveCamera
+ - Class: rviz_default_plugins/Select
+ - Class: rviz_default_plugins/FocusCamera
+ - Class: rviz_default_plugins/Measure
+ Line color: 128; 128; 0
+ - Class: rviz_default_plugins/SetInitialPose
+ Covariance x: 0.25
+ Covariance y: 0.25
+ Covariance yaw: 0.06853891909122467
+ Topic:
+ Depth: 5
+ Durability Policy: Volatile
+ History Policy: Keep Last
+ Reliability Policy: Reliable
+ Value: /initialpose
+ - Class: rviz_default_plugins/SetGoal
+ Topic:
+ Depth: 5
+ Durability Policy: Volatile
+ History Policy: Keep Last
+ Reliability Policy: Reliable
+ Value: /goal_pose
+ - Class: rviz_default_plugins/PublishPoint
+ Single click: true
+ Topic:
+ Depth: 5
+ Durability Policy: Volatile
+ History Policy: Keep Last
+ Reliability Policy: Reliable
+ Value: /clicked_point
+ Transformation:
+ Current:
+ Class: rviz_default_plugins/TF
+ Value: true
+ Views:
+ Current:
+ Class: rviz_default_plugins/Orbit
+ Distance: 10.90223503112793
+ Enable Stereo Rendering:
+ Stereo Eye Separation: 0.05999999865889549
+ Stereo Focal Distance: 1
+ Swap Stereo Eyes: false
+ Value: false
+ Focal Point:
+ X: 2.115082263946533
+ Y: 3.9060535430908203
+ Z: 8.745500564575195
+ Focal Shape Fixed Size: true
+ Focal Shape Size: 0.05000000074505806
+ Invert Z Axis: false
+ Name: Current View
+ Near Clip Distance: 0.009999999776482582
+ Pitch: 0.6797969341278076
+ Target Frame:
+ Value: Orbit (rviz)
+ Yaw: 0.14245237410068512
+ Saved: ~
+Window Geometry:
+ Displays:
+ collapsed: false
+ Height: 1368
+ Hide Left Dock: false
+ Hide Right Dock: false
+ Navigation 2:
+ collapsed: false
+ QMainWindow State: 000000ff00000000fd0000000400000000000001a900000502fc0200000009fb000000100044006900730070006c006100790073010000003b000003d9000000c700fffffffb0000001200530065006c0065006300740069006f006e00000004b30000008a0000005c00fffffffb0000001e0054006f006f006c002000500072006f007000650072007400690065007302000001ed000001df00000185000000a3fb000000120056006900650077007300200054006f006f02000001df000002110000018500000122fb000000200054006f006f006c002000500072006f0070006500720074006900650073003203000002880000011d000002210000017afb0000002000730065006c0065006300740069006f006e00200062007500660066006500720200000138000000aa0000023a00000294fb00000014005700690064006500530074006500720065006f02000000e6000000d2000003ee0000030bfb0000000c004b0069006e0065006300740200000186000001060000030c00000261fb00000018004e0061007600690067006100740069006f006e00200032010000041a000001230000011200ffffff000000010000010000000502fc0200000003fb0000001e0054006f006f006c002000500072006f00700065007200740069006500730100000041000000780000000000000000fb0000000a00560069006500770073010000003b00000502000000a000fffffffb0000001200530065006c0065006300740069006f006e010000025a000000b200000000000000000000000200000a000000015afc0100000001fb0000000a00560069006500770073030000004e00000080000002e10000019700000003000003010000005afc0100000002fb0000000800540069006d00650000000000000003010000025300fffffffb0000000800540069006d006501000000000000045000000000000000000000074b0000050200000004000000040000000800000008fc0000000100000002000000010000000a0054006f006f006c00730100000000ffffffff0000000000000000
+ Selection:
+ collapsed: false
+ Time:
+ collapsed: false
+ Tool Properties:
+ collapsed: false
+ Views:
+ collapsed: false
+ Width: 2560
+ X: 0
+ Y: 0
diff --git a/rust/provider/tiago_demo_package/start_rviz.sh b/rust/provider/tiago_demo_package/start_rviz.sh
new file mode 100755
index 0000000..be3dd49
--- /dev/null
+++ b/rust/provider/tiago_demo_package/start_rviz.sh
@@ -0,0 +1,3 @@
+source /opt/ros/humble/setup.sh
+
+rviz2 -d ./rviz.config.rviz
diff --git a/rust/robonix-cli/Cargo.lock b/rust/robonix-cli/Cargo.lock
index de1d709..e0fa4b7 100644
--- a/rust/robonix-cli/Cargo.lock
+++ b/rust/robonix-cli/Cargo.lock
@@ -2,6 +2,12 @@
# It is not intended for manual editing.
version = 4
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
[[package]]
name = "aho-corasick"
version = "1.1.4"
@@ -11,6 +17,24 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "aligned"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685"
+dependencies = [
+ "as-slice",
+]
+
+[[package]]
+name = "aligned-vec"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b"
+dependencies = [
+ "equator",
+]
+
[[package]]
name = "android_system_properties"
version = "0.1.5"
@@ -85,6 +109,38 @@ version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
+[[package]]
+name = "arbitrary"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
+
+[[package]]
+name = "arg_enum_proc_macro"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.109",
+]
+
+[[package]]
+name = "arrayvec"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
+
+[[package]]
+name = "as-slice"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516"
+dependencies = [
+ "stable_deref_trait",
+]
+
[[package]]
name = "async-channel"
version = "2.5.0"
@@ -198,12 +254,60 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "async-stream"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
+dependencies = [
+ "async-stream-impl",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-stream-impl"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.109",
+]
+
[[package]]
name = "async-task"
version = "4.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
+[[package]]
+name = "async-trait"
+version = "0.1.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.109",
+]
+
+[[package]]
+name = "atomic"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba"
+
+[[package]]
+name = "atomic"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340"
+dependencies = [
+ "bytemuck",
+]
+
[[package]]
name = "atomic-waker"
version = "1.1.2"
@@ -216,18 +320,95 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+[[package]]
+name = "av-scenechange"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394"
+dependencies = [
+ "aligned",
+ "anyhow",
+ "arg_enum_proc_macro",
+ "arrayvec",
+ "log",
+ "num-rational",
+ "num-traits",
+ "pastey",
+ "rayon",
+ "thiserror 2.0.17",
+ "v_frame",
+ "y4m",
+]
+
+[[package]]
+name = "av1-grain"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8"
+dependencies = [
+ "anyhow",
+ "arrayvec",
+ "log",
+ "nom",
+ "num-rational",
+ "v_frame",
+]
+
+[[package]]
+name = "avif-serialize"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47c8fbc0f831f4519fe8b810b6a7a91410ec83031b8233f730a0480029f6a23f"
+dependencies = [
+ "arrayvec",
+]
+
+[[package]]
+name = "aws-lc-rs"
+version = "1.15.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b7b6141e96a8c160799cc2d5adecd5cbbe5054cb8c7c4af53da0f83bb7ad256"
+dependencies = [
+ "aws-lc-sys",
+ "zeroize",
+]
+
+[[package]]
+name = "aws-lc-sys"
+version = "0.37.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c34dda4df7017c8db52132f0f8a2e0f8161649d15723ed63fc00c82d0f2081a"
+dependencies = [
+ "cc",
+ "cmake",
+ "dunce",
+ "fs_extra",
+]
+
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+[[package]]
+name = "binascii"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72"
+
[[package]]
name = "bit-vec"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
+[[package]]
+name = "bit_field"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6"
+
[[package]]
name = "bitflags"
version = "1.3.2"
@@ -240,6 +421,15 @@ version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
+[[package]]
+name = "bitstream-io"
+version = "4.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757"
+dependencies = [
+ "core2",
+]
+
[[package]]
name = "blocking"
version = "1.6.2"
@@ -264,18 +454,36 @@ dependencies = [
"serde",
]
+[[package]]
+name = "built"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64"
+
[[package]]
name = "bumpalo"
version = "3.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
+[[package]]
+name = "bytemuck"
+version = "1.24.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4"
+
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+[[package]]
+name = "byteorder-lite"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
+
[[package]]
name = "bytes"
version = "1.10.1"
@@ -287,9 +495,9 @@ dependencies = [
[[package]]
name = "cc"
-version = "1.2.45"
+version = "1.2.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "35900b6c8d709fb1d854671ae27aeaa9eec2f8b01b364e1619a40da3e6fe2afe"
+checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -332,6 +540,12 @@ dependencies = [
"syn 1.0.109",
]
+[[package]]
+name = "cesu8"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
+
[[package]]
name = "cfg-if"
version = "0.1.10"
@@ -404,6 +618,21 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
+[[package]]
+name = "cmake"
+version = "0.1.57"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "color_quant"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
+
[[package]]
name = "colorchoice"
version = "1.0.4"
@@ -419,6 +648,16 @@ dependencies = [
"windows-sys 0.59.0",
]
+[[package]]
+name = "combine"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
+dependencies = [
+ "bytes",
+ "memchr",
+]
+
[[package]]
name = "concurrent-queue"
version = "2.5.0"
@@ -428,6 +667,17 @@ dependencies = [
"crossbeam-utils",
]
+[[package]]
+name = "cookie"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
+dependencies = [
+ "percent-encoding",
+ "time",
+ "version_check",
+]
+
[[package]]
name = "core-foundation"
version = "0.9.4"
@@ -438,18 +688,113 @@ dependencies = [
"libc",
]
+[[package]]
+name = "core-foundation"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+[[package]]
+name = "core2"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if 1.0.4",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+[[package]]
+name = "crunchy"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
+
+[[package]]
+name = "deranged"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587"
+dependencies = [
+ "powerfmt",
+]
+
+[[package]]
+name = "devise"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1d90b0c4c777a2cad215e3c7be59ac7c15adf45cf76317009b7d096d46f651d"
+dependencies = [
+ "devise_codegen",
+ "devise_core",
+]
+
+[[package]]
+name = "devise_codegen"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71b28680d8be17a570a2334922518be6adc3f58ecc880cbb404eaeb8624fd867"
+dependencies = [
+ "devise_core",
+ "quote",
+]
+
+[[package]]
+name = "devise_core"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b035a542cf7abf01f2e3c4d5a7acbaebfefe120ae4efc7bde3df98186e4b8af7"
+dependencies = [
+ "bitflags 2.10.0",
+ "proc-macro2",
+ "proc-macro2-diagnostics",
+ "quote",
+ "syn 2.0.109",
+]
+
[[package]]
name = "dirs"
version = "6.0.0"
@@ -482,6 +827,12 @@ dependencies = [
"syn 2.0.109",
]
+[[package]]
+name = "dunce"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
+
[[package]]
name = "either"
version = "1.15.0"
@@ -541,6 +892,26 @@ dependencies = [
"log",
]
+[[package]]
+name = "equator"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc"
+dependencies = [
+ "equator-macro",
+]
+
+[[package]]
+name = "equator-macro"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.109",
+]
+
[[package]]
name = "equivalent"
version = "1.0.2"
@@ -578,6 +949,21 @@ dependencies = [
"pin-project-lite",
]
+[[package]]
+name = "exr"
+version = "1.74.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be"
+dependencies = [
+ "bit_field",
+ "half",
+ "lebe",
+ "miniz_oxide",
+ "rayon-core",
+ "smallvec",
+ "zune-inflate",
+]
+
[[package]]
name = "fastrand"
version = "2.3.0"
@@ -585,31 +971,69 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
-name = "find-msvc-tools"
-version = "0.1.4"
+name = "fax"
+version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127"
+checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab"
+dependencies = [
+ "fax_derive",
+]
[[package]]
-name = "fnv"
-version = "1.0.7"
+name = "fax_derive"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.109",
+]
[[package]]
-name = "foreign-types"
-version = "0.3.2"
+name = "fdeflate"
+version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
dependencies = [
- "foreign-types-shared",
+ "simd-adler32",
]
[[package]]
-name = "foreign-types-shared"
-version = "0.1.1"
+name = "figment"
+version = "0.10.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3"
+dependencies = [
+ "atomic 0.6.1",
+ "pear",
+ "serde",
+ "toml",
+ "uncased",
+ "version_check",
+]
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db"
+
+[[package]]
+name = "flate2"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "form_urlencoded"
@@ -620,6 +1044,12 @@ dependencies = [
"percent-encoding",
]
+[[package]]
+name = "fs_extra"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
+
[[package]]
name = "fuchsia-zircon"
version = "0.3.3"
@@ -738,6 +1168,19 @@ dependencies = [
"slab",
]
+[[package]]
+name = "generator"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e"
+dependencies = [
+ "cc",
+ "libc",
+ "log",
+ "rustversion",
+ "windows",
+]
+
[[package]]
name = "getrandom"
version = "0.2.16"
@@ -745,8 +1188,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [
"cfg-if 1.0.4",
+ "js-sys",
"libc",
"wasi",
+ "wasm-bindgen",
]
[[package]]
@@ -756,9 +1201,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if 1.0.4",
+ "js-sys",
"libc",
"r-efi",
"wasip2",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "gif"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e"
+dependencies = [
+ "color_quant",
+ "weezl",
]
[[package]]
@@ -771,7 +1228,7 @@ dependencies = [
"libc",
"libgit2-sys",
"log",
- "openssl-probe",
+ "openssl-probe 0.1.6",
"openssl-sys",
"url",
]
@@ -782,6 +1239,25 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
+[[package]]
+name = "h2"
+version = "0.3.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d"
+dependencies = [
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http 0.2.12",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
[[package]]
name = "h2"
version = "0.4.12"
@@ -793,7 +1269,7 @@ dependencies = [
"fnv",
"futures-core",
"futures-sink",
- "http",
+ "http 1.3.1",
"indexmap",
"slab",
"tokio",
@@ -801,6 +1277,17 @@ dependencies = [
"tracing",
]
+[[package]]
+name = "half"
+version = "2.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
+dependencies = [
+ "cfg-if 1.0.4",
+ "crunchy",
+ "zerocopy",
+]
+
[[package]]
name = "hashbrown"
version = "0.16.0"
@@ -830,6 +1317,17 @@ dependencies = [
"windows-link 0.1.3",
]
+[[package]]
+name = "http"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
[[package]]
name = "http"
version = "1.3.1"
@@ -841,6 +1339,17 @@ dependencies = [
"itoa",
]
+[[package]]
+name = "http-body"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
+dependencies = [
+ "bytes",
+ "http 0.2.12",
+ "pin-project-lite",
+]
+
[[package]]
name = "http-body"
version = "1.0.1"
@@ -848,7 +1357,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
dependencies = [
"bytes",
- "http",
+ "http 1.3.1",
]
[[package]]
@@ -859,8 +1368,8 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
dependencies = [
"bytes",
"futures-core",
- "http",
- "http-body",
+ "http 1.3.1",
+ "http-body 1.0.1",
"pin-project-lite",
]
@@ -870,6 +1379,36 @@ version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "hyper"
+version = "0.14.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2 0.3.27",
+ "http 0.2.12",
+ "http-body 0.4.6",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "socket2 0.5.10",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "want",
+]
+
[[package]]
name = "hyper"
version = "1.8.1"
@@ -880,9 +1419,9 @@ dependencies = [
"bytes",
"futures-channel",
"futures-core",
- "h2",
- "http",
- "http-body",
+ "h2 0.4.12",
+ "http 1.3.1",
+ "http-body 1.0.1",
"httparse",
"itoa",
"pin-project-lite",
@@ -898,8 +1437,8 @@ version = "0.27.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
dependencies = [
- "http",
- "hyper",
+ "http 1.3.1",
+ "hyper 1.8.1",
"hyper-util",
"rustls",
"rustls-pki-types",
@@ -908,22 +1447,6 @@ dependencies = [
"tower-service",
]
-[[package]]
-name = "hyper-tls"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
-dependencies = [
- "bytes",
- "http-body-util",
- "hyper",
- "hyper-util",
- "native-tls",
- "tokio",
- "tokio-native-tls",
- "tower-service",
-]
-
[[package]]
name = "hyper-util"
version = "0.1.18"
@@ -935,9 +1458,9 @@ dependencies = [
"futures-channel",
"futures-core",
"futures-util",
- "http",
- "http-body",
- "hyper",
+ "http 1.3.1",
+ "http-body 1.0.1",
+ "hyper 1.8.1",
"ipnet",
"libc",
"percent-encoding",
@@ -1086,6 +1609,46 @@ dependencies = [
"windows-sys 0.59.0",
]
+[[package]]
+name = "image"
+version = "0.25.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a"
+dependencies = [
+ "bytemuck",
+ "byteorder-lite",
+ "color_quant",
+ "exr",
+ "gif",
+ "image-webp",
+ "moxcms",
+ "num-traits",
+ "png",
+ "qoi",
+ "ravif",
+ "rayon",
+ "rgb",
+ "tiff",
+ "zune-core 0.5.0",
+ "zune-jpeg 0.5.8",
+]
+
+[[package]]
+name = "image-webp"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3"
+dependencies = [
+ "byteorder-lite",
+ "quick-error",
+]
+
+[[package]]
+name = "imgref"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8"
+
[[package]]
name = "indexmap"
version = "2.12.0"
@@ -1094,6 +1657,25 @@ checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f"
dependencies = [
"equivalent",
"hashbrown",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "inlinable_string"
+version = "0.1.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb"
+
+[[package]]
+name = "interpolate_name"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.109",
]
[[package]]
@@ -1146,6 +1728,17 @@ dependencies = [
"serde",
]
+[[package]]
+name = "is-terminal"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
@@ -1191,6 +1784,28 @@ dependencies = [
"syn 2.0.109",
]
+[[package]]
+name = "jni"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
+dependencies = [
+ "cesu8",
+ "cfg-if 1.0.4",
+ "combine",
+ "jni-sys",
+ "log",
+ "thiserror 1.0.69",
+ "walkdir",
+ "windows-sys 0.45.0",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
+
[[package]]
name = "jobserver"
version = "0.1.34"
@@ -1233,11 +1848,27 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
+[[package]]
+name = "lebe"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8"
+
[[package]]
name = "libc"
-version = "0.2.177"
+version = "0.2.180"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
+checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
+
+[[package]]
+name = "libfuzzer-sys"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404"
+dependencies = [
+ "arbitrary",
+ "cc",
+]
[[package]]
name = "libgit2-sys"
@@ -1328,6 +1959,55 @@ version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
+[[package]]
+name = "loom"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5"
+dependencies = [
+ "cfg-if 1.0.4",
+ "generator",
+ "scoped-tls",
+ "serde",
+ "serde_json",
+ "tracing",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "loop9"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062"
+dependencies = [
+ "imgref",
+]
+
+[[package]]
+name = "lru-slab"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
+
+[[package]]
+name = "matchers"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
+dependencies = [
+ "regex-automata",
+]
+
+[[package]]
+name = "maybe-rayon"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519"
+dependencies = [
+ "cfg-if 1.0.4",
+ "rayon",
+]
+
[[package]]
name = "md5"
version = "0.8.0"
@@ -1355,6 +2035,16 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
[[package]]
name = "mio"
version = "0.6.23"
@@ -1422,20 +2112,32 @@ dependencies = [
]
[[package]]
-name = "native-tls"
-version = "0.2.14"
+name = "moxcms"
+version = "0.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
+checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97"
dependencies = [
- "libc",
- "log",
- "openssl",
- "openssl-probe",
- "openssl-sys",
- "schannel",
- "security-framework",
- "security-framework-sys",
- "tempfile",
+ "num-traits",
+ "pxfm",
+]
+
+[[package]]
+name = "multer"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b"
+dependencies = [
+ "bytes",
+ "encoding_rs",
+ "futures-util",
+ "http 1.3.1",
+ "httparse",
+ "memchr",
+ "mime",
+ "spin",
+ "tokio",
+ "tokio-util",
+ "version_check",
]
[[package]]
@@ -1474,11 +2176,17 @@ dependencies = [
"winapi 0.3.9",
]
+[[package]]
+name = "new_debug_unreachable"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
+
[[package]]
name = "nix"
-version = "0.30.1"
+version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
+checksum = "225e7cfe711e0ba79a68baeddb2982723e4235247aefce1482f2f16c27865b66"
dependencies = [
"bitflags 2.10.0",
"cfg-if 1.0.4",
@@ -1501,6 +2209,37 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "noop_proc_macro"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.50.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+
[[package]]
name = "num-derive"
version = "0.4.2"
@@ -1513,51 +2252,55 @@ dependencies = [
]
[[package]]
-name = "num-traits"
-version = "0.2.19"
+name = "num-integer"
+version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
dependencies = [
- "autocfg",
+ "num-traits",
]
[[package]]
-name = "once_cell"
-version = "1.21.3"
+name = "num-rational"
+version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
+dependencies = [
+ "num-bigint",
+ "num-integer",
+ "num-traits",
+]
[[package]]
-name = "once_cell_polyfill"
-version = "1.70.2"
+name = "num-traits"
+version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
[[package]]
-name = "openssl"
-version = "0.10.75"
+name = "num_cpus"
+version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
+checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
dependencies = [
- "bitflags 2.10.0",
- "cfg-if 1.0.4",
- "foreign-types",
+ "hermit-abi",
"libc",
- "once_cell",
- "openssl-macros",
- "openssl-sys",
]
[[package]]
-name = "openssl-macros"
-version = "0.1.1"
+name = "once_cell"
+version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.109",
-]
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "openssl-probe"
@@ -1565,6 +2308,12 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
+[[package]]
+name = "openssl-probe"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
+
[[package]]
name = "openssl-sys"
version = "0.9.111"
@@ -1618,12 +2367,41 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+[[package]]
+name = "pastey"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
+
[[package]]
name = "pathdiff"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
+[[package]]
+name = "pear"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467"
+dependencies = [
+ "inlinable_string",
+ "pear_codegen",
+ "yansi",
+]
+
+[[package]]
+name = "pear_codegen"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147"
+dependencies = [
+ "proc-macro2",
+ "proc-macro2-diagnostics",
+ "quote",
+ "syn 2.0.109",
+]
+
[[package]]
name = "percent-encoding"
version = "2.3.2"
@@ -1750,6 +2528,19 @@ dependencies = [
"pnet_sys",
]
+[[package]]
+name = "png"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0"
+dependencies = [
+ "bitflags 2.10.0",
+ "crc32fast",
+ "fdeflate",
+ "flate2",
+ "miniz_oxide",
+]
+
[[package]]
name = "polling"
version = "3.11.0"
@@ -1788,6 +2579,12 @@ dependencies = [
"zerovec",
]
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
[[package]]
name = "ppv-lite86"
version = "0.2.21"
@@ -1806,6 +2603,118 @@ dependencies = [
"unicode-ident",
]
+[[package]]
+name = "proc-macro2-diagnostics"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.109",
+ "version_check",
+ "yansi",
+]
+
+[[package]]
+name = "profiling"
+version = "1.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773"
+dependencies = [
+ "profiling-procmacros",
+]
+
+[[package]]
+name = "profiling-procmacros"
+version = "1.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b"
+dependencies = [
+ "quote",
+ "syn 2.0.109",
+]
+
+[[package]]
+name = "pxfm"
+version = "0.1.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "qoi"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001"
+dependencies = [
+ "bytemuck",
+]
+
+[[package]]
+name = "quick-error"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
+
+[[package]]
+name = "quinn"
+version = "0.11.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
+dependencies = [
+ "bytes",
+ "cfg_aliases",
+ "pin-project-lite",
+ "quinn-proto",
+ "quinn-udp",
+ "rustc-hash",
+ "rustls",
+ "socket2 0.6.1",
+ "thiserror 2.0.17",
+ "tokio",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-proto"
+version = "0.11.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31"
+dependencies = [
+ "aws-lc-rs",
+ "bytes",
+ "getrandom 0.3.4",
+ "lru-slab",
+ "rand 0.9.2",
+ "ring",
+ "rustc-hash",
+ "rustls",
+ "rustls-pki-types",
+ "slab",
+ "thiserror 2.0.17",
+ "tinyvec",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-udp"
+version = "0.5.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
+dependencies = [
+ "cfg_aliases",
+ "libc",
+ "once_cell",
+ "socket2 0.6.1",
+ "tracing",
+ "windows-sys 0.60.2",
+]
+
[[package]]
name = "quote"
version = "1.0.42"
@@ -1821,14 +2730,35 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
+]
+
[[package]]
name = "rand"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
- "rand_chacha",
- "rand_core",
+ "rand_chacha 0.9.0",
+ "rand_core 0.9.3",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.6.4",
]
[[package]]
@@ -1838,7 +2768,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
- "rand_core",
+ "rand_core 0.9.3",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.16",
]
[[package]]
@@ -1850,6 +2789,76 @@ dependencies = [
"getrandom 0.3.4",
]
+[[package]]
+name = "rav1e"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b"
+dependencies = [
+ "aligned-vec",
+ "arbitrary",
+ "arg_enum_proc_macro",
+ "arrayvec",
+ "av-scenechange",
+ "av1-grain",
+ "bitstream-io",
+ "built",
+ "cfg-if 1.0.4",
+ "interpolate_name",
+ "itertools",
+ "libc",
+ "libfuzzer-sys",
+ "log",
+ "maybe-rayon",
+ "new_debug_unreachable",
+ "noop_proc_macro",
+ "num-derive",
+ "num-traits",
+ "paste",
+ "profiling",
+ "rand 0.9.2",
+ "rand_chacha 0.9.0",
+ "simd_helpers",
+ "thiserror 2.0.17",
+ "v_frame",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "ravif"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef69c1990ceef18a116855938e74793a5f7496ee907562bd0857b6ac734ab285"
+dependencies = [
+ "avif-serialize",
+ "imgref",
+ "loop9",
+ "quick-error",
+ "rav1e",
+ "rayon",
+ "rgb",
+]
+
+[[package]]
+name = "rayon"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
+dependencies = [
+ "crossbeam-deque",
+ "crossbeam-utils",
+]
+
[[package]]
name = "redox_syscall"
version = "0.5.18"
@@ -1870,6 +2879,26 @@ dependencies = [
"thiserror 2.0.17",
]
+[[package]]
+name = "ref-cast"
+version = "1.0.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d"
+dependencies = [
+ "ref-cast-impl",
+]
+
+[[package]]
+name = "ref-cast-impl"
+version = "1.0.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.109",
+]
+
[[package]]
name = "regex"
version = "1.12.2"
@@ -1901,37 +2930,35 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "reqwest"
-version = "0.12.24"
+version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f"
+checksum = "04e9018c9d814e5f30cc16a0f03271aeab3571e609612d9fe78c1aa8d11c2f62"
dependencies = [
"base64",
"bytes",
"encoding_rs",
- "futures-channel",
"futures-core",
- "futures-util",
- "h2",
- "http",
- "http-body",
+ "h2 0.4.12",
+ "http 1.3.1",
+ "http-body 1.0.1",
"http-body-util",
- "hyper",
+ "hyper 1.8.1",
"hyper-rustls",
- "hyper-tls",
"hyper-util",
"js-sys",
"log",
"mime",
- "native-tls",
"percent-encoding",
"pin-project-lite",
+ "quinn",
+ "rustls",
"rustls-pki-types",
+ "rustls-platform-verifier",
"serde",
"serde_json",
- "serde_urlencoded",
"sync_wrapper",
"tokio",
- "tokio-native-tls",
+ "tokio-rustls",
"tower",
"tower-http",
"tower-service",
@@ -1941,6 +2968,12 @@ dependencies = [
"web-sys",
]
+[[package]]
+name = "rgb"
+version = "0.8.52"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce"
+
[[package]]
name = "ring"
version = "0.17.14"
@@ -1979,6 +3012,7 @@ dependencies = [
"serde_json",
"serde_yaml",
"tokio",
+ "urlencoding",
]
[[package]]
@@ -1986,17 +3020,109 @@ name = "robonix-core"
version = "0.1.0"
dependencies = [
"ansi_term",
+ "anyhow",
+ "base64",
+ "byteorder",
"chrono",
+ "dirs",
"env_logger",
"futures",
"futures-util",
+ "image",
+ "libc",
"log",
+ "regex",
"reqwest",
+ "rocket",
"ros2-client",
"serde",
"serde_json",
+ "serde_yaml",
"smol",
"tokio",
+ "tokio-stream",
+]
+
+[[package]]
+name = "rocket"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a516907296a31df7dc04310e7043b61d71954d703b603cc6867a026d7e72d73f"
+dependencies = [
+ "async-stream",
+ "async-trait",
+ "atomic 0.5.3",
+ "binascii",
+ "bytes",
+ "either",
+ "figment",
+ "futures",
+ "indexmap",
+ "log",
+ "memchr",
+ "multer",
+ "num_cpus",
+ "parking_lot",
+ "pin-project-lite",
+ "rand 0.8.5",
+ "ref-cast",
+ "rocket_codegen",
+ "rocket_http",
+ "serde",
+ "serde_json",
+ "state",
+ "tempfile",
+ "time",
+ "tokio",
+ "tokio-stream",
+ "tokio-util",
+ "ubyte",
+ "version_check",
+ "yansi",
+]
+
+[[package]]
+name = "rocket_codegen"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "575d32d7ec1a9770108c879fc7c47815a80073f96ca07ff9525a94fcede1dd46"
+dependencies = [
+ "devise",
+ "glob",
+ "indexmap",
+ "proc-macro2",
+ "quote",
+ "rocket_http",
+ "syn 2.0.109",
+ "unicode-xid",
+ "version_check",
+]
+
+[[package]]
+name = "rocket_http"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e274915a20ee3065f611c044bd63c40757396b6dbc057d6046aec27f14f882b9"
+dependencies = [
+ "cookie",
+ "either",
+ "futures",
+ "http 0.2.12",
+ "hyper 0.14.32",
+ "indexmap",
+ "log",
+ "memchr",
+ "pear",
+ "percent-encoding",
+ "pin-project-lite",
+ "ref-cast",
+ "serde",
+ "smallvec",
+ "stable-pattern",
+ "state",
+ "time",
+ "tokio",
+ "uncased",
]
[[package]]
@@ -2028,6 +3154,12 @@ dependencies = [
"widestring",
]
+[[package]]
+name = "rustc-hash"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
+
[[package]]
name = "rustdds"
version = "0.11.7"
@@ -2055,7 +3187,7 @@ dependencies = [
"paste",
"pnet",
"pnet_sys",
- "rand",
+ "rand 0.9.2",
"serde",
"serde_repr",
"socket2 0.5.10",
@@ -2084,6 +3216,7 @@ version = "0.23.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f"
dependencies = [
+ "aws-lc-rs",
"once_cell",
"rustls-pki-types",
"rustls-webpki",
@@ -2091,21 +3224,62 @@ dependencies = [
"zeroize",
]
+[[package]]
+name = "rustls-native-certs"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63"
+dependencies = [
+ "openssl-probe 0.2.1",
+ "rustls-pki-types",
+ "schannel",
+ "security-framework",
+]
+
[[package]]
name = "rustls-pki-types"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a"
dependencies = [
+ "web-time",
"zeroize",
]
+[[package]]
+name = "rustls-platform-verifier"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
+dependencies = [
+ "core-foundation 0.10.1",
+ "core-foundation-sys",
+ "jni",
+ "log",
+ "once_cell",
+ "rustls",
+ "rustls-native-certs",
+ "rustls-platform-verifier-android",
+ "rustls-webpki",
+ "security-framework",
+ "security-framework-sys",
+ "webpki-root-certs",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustls-platform-verifier-android"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
+
[[package]]
name = "rustls-webpki"
version = "0.103.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52"
dependencies = [
+ "aws-lc-rs",
"ring",
"rustls-pki-types",
"untrusted",
@@ -2123,6 +3297,15 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
[[package]]
name = "schannel"
version = "0.1.28"
@@ -2132,6 +3315,12 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "scoped-tls"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
+
[[package]]
name = "scopeguard"
version = "1.2.0"
@@ -2140,12 +3329,12 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "security-framework"
-version = "2.11.1"
+version = "3.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
+checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef"
dependencies = [
"bitflags 2.10.0",
- "core-foundation",
+ "core-foundation 0.10.1",
"core-foundation-sys",
"libc",
"security-framework-sys",
@@ -2216,14 +3405,11 @@ dependencies = [
]
[[package]]
-name = "serde_urlencoded"
-version = "0.7.1"
+name = "serde_spanned"
+version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
dependencies = [
- "form_urlencoded",
- "itoa",
- "ryu",
"serde",
]
@@ -2240,6 +3426,15 @@ dependencies = [
"unsafe-libyaml",
]
+[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
[[package]]
name = "shlex"
version = "1.3.0"
@@ -2250,9 +3445,24 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
name = "signal-hook-registry"
version = "1.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b"
+checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
+
+[[package]]
+name = "simd_helpers"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6"
dependencies = [
- "libc",
+ "quote",
]
[[package]]
@@ -2338,12 +3548,36 @@ dependencies = [
"syn 2.0.109",
]
+[[package]]
+name = "spin"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+
+[[package]]
+name = "stable-pattern"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4564168c00635f88eaed410d5efa8131afa8d8699a612c80c455a0ba05c21045"
+dependencies = [
+ "memchr",
+]
+
[[package]]
name = "stable_deref_trait"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+[[package]]
+name = "state"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b8c4a4445d81357df8b1a650d0d0d6fbbbfe99d064aa5e02f3e4022061476d8"
+dependencies = [
+ "loom",
+]
+
[[package]]
name = "static_assertions"
version = "1.1.0"
@@ -2411,7 +3645,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
dependencies = [
"bitflags 1.3.2",
- "core-foundation",
+ "core-foundation 0.9.4",
"system-configuration-sys",
]
@@ -2478,6 +3712,60 @@ dependencies = [
"syn 2.0.109",
]
+[[package]]
+name = "thread_local"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
+dependencies = [
+ "cfg-if 1.0.4",
+]
+
+[[package]]
+name = "tiff"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f"
+dependencies = [
+ "fax",
+ "flate2",
+ "half",
+ "quick-error",
+ "weezl",
+ "zune-jpeg 0.4.21",
+]
+
+[[package]]
+name = "time"
+version = "0.3.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d"
+dependencies = [
+ "deranged",
+ "itoa",
+ "num-conv",
+ "powerfmt",
+ "serde",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
+
+[[package]]
+name = "time-macros"
+version = "0.2.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
[[package]]
name = "tinystr"
version = "0.8.2"
@@ -2488,11 +3776,26 @@ dependencies = [
"zerovec",
]
+[[package]]
+name = "tinyvec"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
[[package]]
name = "tokio"
-version = "1.48.0"
+version = "1.49.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
+checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
dependencies = [
"bytes",
"libc",
@@ -2517,22 +3820,23 @@ dependencies = [
]
[[package]]
-name = "tokio-native-tls"
-version = "0.3.1"
+name = "tokio-rustls"
+version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
+checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
dependencies = [
- "native-tls",
+ "rustls",
"tokio",
]
[[package]]
-name = "tokio-rustls"
-version = "0.26.4"
+name = "tokio-stream"
+version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
+checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
dependencies = [
- "rustls",
+ "futures-core",
+ "pin-project-lite",
"tokio",
]
@@ -2549,6 +3853,47 @@ dependencies = [
"tokio",
]
+[[package]]
+name = "toml"
+version = "0.8.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
+dependencies = [
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.22.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
+dependencies = [
+ "indexmap",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_write",
+ "winnow",
+]
+
+[[package]]
+name = "toml_write"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
+
[[package]]
name = "tower"
version = "0.5.2"
@@ -2566,15 +3911,15 @@ dependencies = [
[[package]]
name = "tower-http"
-version = "0.6.6"
+version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
+checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
dependencies = [
"bitflags 2.10.0",
"bytes",
"futures-util",
- "http",
- "http-body",
+ "http 1.3.1",
+ "http-body 1.0.1",
"iri-string",
"pin-project-lite",
"tower",
@@ -2623,6 +3968,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
dependencies = [
"once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
+dependencies = [
+ "matchers",
+ "nu-ansi-term",
+ "once_cell",
+ "regex-automata",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
]
[[package]]
@@ -2631,12 +4006,37 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+[[package]]
+name = "ubyte"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f720def6ce1ee2fc44d40ac9ed6d3a59c361c80a75a7aa8e75bb9baed31cf2ea"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "uncased"
+version = "0.9.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697"
+dependencies = [
+ "serde",
+ "version_check",
+]
+
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
+[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
@@ -2661,6 +4061,12 @@ dependencies = [
"serde",
]
+[[package]]
+name = "urlencoding"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
+
[[package]]
name = "utf8_iter"
version = "1.0.4"
@@ -2681,7 +4087,7 @@ checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
dependencies = [
"getrandom 0.3.4",
"js-sys",
- "rand",
+ "rand 0.9.2",
"serde",
"uuid-macro-internal",
"wasm-bindgen",
@@ -2698,12 +4104,45 @@ dependencies = [
"syn 2.0.109",
]
+[[package]]
+name = "v_frame"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2"
+dependencies = [
+ "aligned-vec",
+ "num-traits",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "valuable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
[[package]]
name = "want"
version = "0.3.1"
@@ -2796,6 +4235,31 @@ dependencies = [
"wasm-bindgen",
]
+[[package]]
+name = "web-time"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webpki-root-certs"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36a29fc0408b113f68cf32637857ab740edfafdf460c326cd2afaa2d84cc05dc"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "weezl"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
+
[[package]]
name = "widestring"
version = "1.2.1"
@@ -2830,12 +4294,30 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+[[package]]
+name = "winapi-util"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+[[package]]
+name = "windows"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
[[package]]
name = "windows-core"
version = "0.62.2"
@@ -2912,6 +4394,15 @@ dependencies = [
"windows-link 0.2.1",
]
+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets 0.42.2",
+]
+
[[package]]
name = "windows-sys"
version = "0.48.0"
@@ -2957,6 +4448,21 @@ dependencies = [
"windows-link 0.2.1",
]
+[[package]]
+name = "windows-targets"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
+dependencies = [
+ "windows_aarch64_gnullvm 0.42.2",
+ "windows_aarch64_msvc 0.42.2",
+ "windows_i686_gnu 0.42.2",
+ "windows_i686_msvc 0.42.2",
+ "windows_x86_64_gnu 0.42.2",
+ "windows_x86_64_gnullvm 0.42.2",
+ "windows_x86_64_msvc 0.42.2",
+]
+
[[package]]
name = "windows-targets"
version = "0.48.5"
@@ -3005,6 +4511,12 @@ dependencies = [
"windows_x86_64_msvc 0.53.1",
]
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
+
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
@@ -3023,6 +4535,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
@@ -3041,6 +4559,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
@@ -3071,6 +4595,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
@@ -3089,6 +4619,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
@@ -3107,6 +4643,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
@@ -3125,6 +4667,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
@@ -3143,6 +4691,15 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
+[[package]]
+name = "winnow"
+version = "0.7.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
+dependencies = [
+ "memchr",
+]
+
[[package]]
name = "wit-bindgen"
version = "0.46.0"
@@ -3165,6 +4722,21 @@ dependencies = [
"winapi-build",
]
+[[package]]
+name = "y4m"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448"
+
+[[package]]
+name = "yansi"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
+dependencies = [
+ "is-terminal",
+]
+
[[package]]
name = "yoke"
version = "0.8.1"
@@ -3267,3 +4839,42 @@ dependencies = [
"quote",
"syn 2.0.109",
]
+
+[[package]]
+name = "zune-core"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
+
+[[package]]
+name = "zune-core"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "111f7d9820f05fd715df3144e254d6fc02ee4088b0644c0ffd0efc9e6d9d2773"
+
+[[package]]
+name = "zune-inflate"
+version = "0.2.54"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02"
+dependencies = [
+ "simd-adler32",
+]
+
+[[package]]
+name = "zune-jpeg"
+version = "0.4.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713"
+dependencies = [
+ "zune-core 0.4.12",
+]
+
+[[package]]
+name = "zune-jpeg"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e35aee689668bf9bd6f6f3a6c60bb29ba1244b3b43adfd50edd554a371da37d5"
+dependencies = [
+ "zune-core 0.5.0",
+]
diff --git a/rust/robonix-cli/Cargo.toml b/rust/robonix-cli/Cargo.toml
index 8776727..c9e3bf8 100644
--- a/rust/robonix-cli/Cargo.toml
+++ b/rust/robonix-cli/Cargo.toml
@@ -1,7 +1,7 @@
[package]
name = "robonix-cli"
version = "0.1.0"
-edition = "2021"
+edition = "2024"
default-run = "rbnx"
[[bin]]
@@ -13,13 +13,14 @@ name = "rbnx-daemon"
path = "src/daemon_main.rs"
[dependencies]
-clap = { version = "4.4", features = ["derive"] }
+clap = { version = "4.5", features = ["derive"] }
serde = { version = "1.0", features = ["derive"] }
serde_yaml = "0.9.33"
serde_json = "1.0"
-tokio = { version = "1.0", features = ["full"] }
+tokio = { version = "1", features = ["full"] }
anyhow = "1.0"
-reqwest = { version = "0.12.24", features = ["json"] }
+reqwest = { version = "0.13", features = ["json"] }
+urlencoding = "2.1"
log = "0.4"
env_logger = "0.11"
dirs = "6.0.0"
@@ -30,7 +31,7 @@ hostname = "0.4"
regex = "1.10"
colored = "3.0.0"
[target.'cfg(unix)'.dependencies]
-nix = { version = "0.30.1", features = ["signal", "process"] }
+nix = { version = "0.31.1", features = ["signal", "process"] }
robonix-core = { path = "../robonix-core" }
ros2-client = { version = "0.8.2", features = ["pre-iron-gid"] }
rustdds = "0.11"
diff --git a/rust/robonix-cli/config.yaml.example b/rust/robonix-cli/config.yaml.example
index 739bef5..6f0e840 100644
--- a/rust/robonix-cli/config.yaml.example
+++ b/rust/robonix-cli/config.yaml.example
@@ -11,3 +11,11 @@ package_storage_path: ~/.robonix/packages
# If not set, will use ROBONIX_SDK_PATH environment variable
# robonix_sdk_path: /path/to/robonix-sdk
+# Node ID (optional) - identifier for this CLI client; shown in core web UI as "Registered by (node)".
+# Default: hostname. Set when multiple machines use the same hostname or you want a custom label.
+# node_id: my-robot-1
+
+# Core web HTTP URL (optional) - base URL of robonix-core web server (e.g. http://core-host:8080).
+# When set, the daemon pushes capability logs to core for any log viewer opened in the web UI (real-time updates).
+# core_http_url: http://192.168.1.10:8080
+
diff --git a/rust/robonix-cli/src/cmd/build.rs b/rust/robonix-cli/src/cmd/build.rs
index 18ce931..f566beb 100644
--- a/rust/robonix-cli/src/cmd/build.rs
+++ b/rust/robonix-cli/src/cmd/build.rs
@@ -4,7 +4,7 @@
// Build command implementation for robonix-cli
use super::recipe_utils;
-use crate::{output, Config, PackageDatabase};
+use crate::{Config, PackageDatabase, output};
use anyhow::{Context, Result};
use colored::*;
use serde_yaml::Value;
diff --git a/rust/robonix-cli/src/cmd/clean.rs b/rust/robonix-cli/src/cmd/clean.rs
new file mode 100644
index 0000000..6c11e4b
--- /dev/null
+++ b/rust/robonix-cli/src/cmd/clean.rs
@@ -0,0 +1,85 @@
+// SPDX-License-Identifier: MulanPSL-2.0
+// Clean Command Module
+//
+// Clean command implementation for robonix-cli
+
+use crate::Config;
+use crate::output;
+use anyhow::{Context, Result};
+use std::fs;
+use std::path::PathBuf;
+
+pub async fn execute(config: Config) -> Result<()> {
+ let logs_dir = config.package_storage_path.join("logs");
+
+ output::action(
+ "Cleaning",
+ &format!("logs directory: {}", logs_dir.display()),
+ );
+
+ // Check if logs directory exists
+ if !logs_dir.exists() {
+ output::info(&format!(
+ "Logs directory does not exist: {}",
+ logs_dir.display()
+ ));
+ return Ok(());
+ }
+
+ if !logs_dir.is_dir() {
+ anyhow::bail!(
+ "Logs path exists but is not a directory: {}",
+ logs_dir.display()
+ );
+ }
+
+ // Count files before deletion
+ let file_count = count_files_in_dir(&logs_dir)?;
+
+ if file_count == 0 {
+ output::info("Logs directory is already empty");
+ return Ok(());
+ }
+
+ output::step("Found", &format!("{} log file(s) to remove", file_count));
+
+ // Remove all files in logs directory
+ remove_files_in_dir(&logs_dir)
+ .with_context(|| format!("Failed to clean logs directory: {}", logs_dir.display()))?;
+
+ output::success(&format!("Cleaned {} log file(s)", file_count));
+
+ Ok(())
+}
+
+fn count_files_in_dir(dir: &PathBuf) -> Result {
+ let mut count = 0;
+ let entries = fs::read_dir(dir)
+ .with_context(|| format!("Failed to read directory: {}", dir.display()))?;
+
+ for entry in entries {
+ let entry = entry.context("Failed to read directory entry")?;
+ let path = entry.path();
+ if path.is_file() {
+ count += 1;
+ }
+ }
+
+ Ok(count)
+}
+
+fn remove_files_in_dir(dir: &PathBuf) -> Result<()> {
+ let entries = fs::read_dir(dir)
+ .with_context(|| format!("Failed to read directory: {}", dir.display()))?;
+
+ for entry in entries {
+ let entry = entry.context("Failed to read directory entry")?;
+ let path = entry.path();
+ if path.is_file() {
+ fs::remove_file(&path)
+ .with_context(|| format!("Failed to remove file: {}", path.display()))?;
+ }
+ }
+
+ Ok(())
+}
diff --git a/rust/robonix-cli/src/cmd/daemon.rs b/rust/robonix-cli/src/cmd/daemon.rs
index da649b5..34f51fa 100644
--- a/rust/robonix-cli/src/cmd/daemon.rs
+++ b/rust/robonix-cli/src/cmd/daemon.rs
@@ -67,7 +67,7 @@ pub async fn stop() -> Result<()> {
// For now, we'll use a simple approach: find the process and kill it
#[cfg(unix)]
{
- use nix::sys::signal::{kill, Signal};
+ use nix::sys::signal::{Signal, kill};
use nix::unistd::Pid;
// Try to find daemon process by checking socket file's owner or by name
diff --git a/rust/robonix-cli/src/cmd/install.rs b/rust/robonix-cli/src/cmd/install.rs
index 457d4e0..ab9e0fc 100644
--- a/rust/robonix-cli/src/cmd/install.rs
+++ b/rust/robonix-cli/src/cmd/install.rs
@@ -3,7 +3,7 @@
//
// Install command implementation for robonix-cli
-use crate::{output, Config, PackageInstaller};
+use crate::{Config, PackageInstaller, output};
use anyhow::Result;
use std::path::PathBuf;
diff --git a/rust/robonix-cli/src/cmd/mod.rs b/rust/robonix-cli/src/cmd/mod.rs
index 7f405c0..581f9e8 100644
--- a/rust/robonix-cli/src/cmd/mod.rs
+++ b/rust/robonix-cli/src/cmd/mod.rs
@@ -10,6 +10,7 @@ use std::path::PathBuf;
use crate::Config;
mod build;
+mod clean;
mod config;
mod daemon;
mod info;
@@ -168,6 +169,10 @@ pub enum DeployCommands {
/// - recipe file path (e.g., "demo_recipe.yaml")
target: String,
},
+ /// Clean logs directory in package storage path
+ ///
+ /// Removes all log files from ~/.robonix/packages/logs directory
+ Clean,
}
#[derive(Subcommand)]
@@ -226,6 +231,7 @@ pub async fn execute(command: Commands, config: Config) -> Result<()> {
DeployCommands::Restart { target } => restart::execute(config, target).await,
DeployCommands::Status => status::execute(config).await,
DeployCommands::Unregister { target } => unregister::execute(config, target).await,
+ DeployCommands::Clean => clean::execute(config).await,
},
Commands::Config {
set_storage_path,
diff --git a/rust/robonix-cli/src/cmd/restart.rs b/rust/robonix-cli/src/cmd/restart.rs
index 2bee11c..053de65 100644
--- a/rust/robonix-cli/src/cmd/restart.rs
+++ b/rust/robonix-cli/src/cmd/restart.rs
@@ -5,10 +5,10 @@
use super::start;
use super::stop;
-use crate::output;
use crate::Config;
+use crate::output;
use anyhow::Result;
-use tokio::time::{sleep, Duration};
+use tokio::time::{Duration, sleep};
pub async fn execute(config: Config, target: String) -> Result<()> {
output::action("Restarting", &format!("item(s) matching: {}", target));
diff --git a/rust/robonix-cli/src/cmd/start.rs b/rust/robonix-cli/src/cmd/start.rs
index f7ca414..966d6b2 100644
--- a/rust/robonix-cli/src/cmd/start.rs
+++ b/rust/robonix-cli/src/cmd/start.rs
@@ -4,10 +4,10 @@
// Start command implementation for robonix-cli
use super::recipe_utils;
+use crate::Config;
use crate::daemon_client::{DaemonClient, DaemonCommand, DaemonResponse};
use crate::database::PackageDatabase;
use crate::output;
-use crate::Config;
use anyhow::Result;
use robonix_core::ros_idl::service_registry::RegisterServiceRequest;
use serde_yaml::Value;
@@ -230,9 +230,10 @@ pub async fn wait_and_register_service(
.ok_or_else(|| anyhow::anyhow!("Service entry not found"))?;
// Wait for ROS2 service to be available (check via ros2 service list)
+ // ROS2 service discovery can take 5-15 seconds, especially on first startup
output::sub_step(&format!("Waiting for service {} to be available...", entry));
- let max_wait = Duration::from_secs(10);
- let check_interval = Duration::from_millis(200);
+ let max_wait = Duration::from_secs(60); // Wait up to 60s for service (e.g. semantic_map)
+ let check_interval = Duration::from_millis(500); // Increased from 200ms to 500ms for less frequent checks
let start_time = std::time::Instant::now();
let sdk_path = config
@@ -240,33 +241,80 @@ pub async fn wait_and_register_service(
.as_ref()
.ok_or_else(|| anyhow::anyhow!("robonix_sdk_path not configured"))?;
+ let mut last_log_elapsed = Duration::from_secs(0);
+ let mut check_count = 0;
+
while start_time.elapsed() < max_wait {
// Check if service is available using ros2 service list
// Use bash -c to source SDK and run ros2 command
let output = Command::new("bash")
.arg("-c")
.arg(format!(
- "source {}/install/setup.bash && ros2 service list",
+ "source {}/install/setup.bash && ros2 service list 2>/dev/null",
sdk_path.display()
))
.output();
+ check_count += 1;
+
if let Ok(output) = output {
- if let Ok(service_list) = String::from_utf8(output.stdout) {
- if service_list.lines().any(|line| line.trim() == entry) {
- output::sub_step(&format!("Service {} is available", entry));
- break;
+ if output.status.success() {
+ if let Ok(service_list) = String::from_utf8(output.stdout) {
+ if service_list.lines().any(|line| line.trim() == entry) {
+ let elapsed = start_time.elapsed();
+ output::sub_step(&format!(
+ "Service {} is available (found after {:.1}s, {} checks)",
+ entry,
+ elapsed.as_secs_f64(),
+ check_count
+ ));
+ break;
+ }
}
}
}
+ // Log progress every 5 seconds
+ let elapsed = start_time.elapsed();
+ if elapsed.as_secs() >= 5 && (elapsed - last_log_elapsed).as_secs() >= 5 {
+ output::sub_step(&format!(
+ "Still waiting for service {}... (elapsed: {:.1}s)",
+ entry,
+ elapsed.as_secs_f64()
+ ));
+ last_log_elapsed = elapsed;
+ }
+
tokio::time::sleep(check_interval).await;
}
if start_time.elapsed() >= max_wait {
+ // Try one more time with verbose output for debugging
+ let debug_output = Command::new("bash")
+ .arg("-c")
+ .arg(format!(
+ "source {}/install/setup.bash && ros2 service list",
+ sdk_path.display()
+ ))
+ .output();
+
+ let debug_info = if let Ok(output) = debug_output {
+ if let Ok(service_list) = String::from_utf8(output.stdout) {
+ format!(
+ "Available services: {}",
+ service_list.lines().take(10).collect::>().join(", ")
+ )
+ } else {
+ "Failed to parse service list".to_string()
+ }
+ } else {
+ "Failed to run ros2 service list".to_string()
+ };
+
return Err(anyhow::anyhow!(
- "Service {} did not become available within 10 seconds",
- entry
+ "Service {} did not become available within 60 seconds. {}",
+ entry,
+ debug_info
));
}
@@ -294,6 +342,7 @@ pub async fn wait_and_register_service(
metadata: metadata_str.to_string(),
provider: package_name.to_string(),
version,
+ node_id: config.effective_node_id(),
};
let request_json = serde_json::to_string(&request)?;
diff --git a/rust/robonix-cli/src/cmd/stop.rs b/rust/robonix-cli/src/cmd/stop.rs
index bbbb21a..fc01814 100644
--- a/rust/robonix-cli/src/cmd/stop.rs
+++ b/rust/robonix-cli/src/cmd/stop.rs
@@ -5,7 +5,7 @@
use super::recipe_utils;
use crate::daemon_client::{DaemonClient, DaemonCommand, DaemonResponse};
-use crate::{output, Config};
+use crate::{Config, output};
use anyhow::Result;
pub async fn execute(config: Config, target: String) -> Result<()> {
diff --git a/rust/robonix-cli/src/cmd/task.rs b/rust/robonix-cli/src/cmd/task.rs
index 2fccb3d..2625735 100644
--- a/rust/robonix-cli/src/cmd/task.rs
+++ b/rust/robonix-cli/src/cmd/task.rs
@@ -99,7 +99,19 @@ pub async fn execute_list(_config: Config) -> Result<()> {
Ok(())
}
-pub async fn execute_cancel(_config: Config, _task_id: String) -> Result<()> {
- output::warning("Cancel task functionality is not yet implemented in the new EAIOS API");
+pub async fn execute_cancel(config: Config, task_id: String) -> Result<()> {
+ output::action("Cancelling", &format!("task {}", task_id));
+
+ let client = TaskClient::new(config)?;
+ let response = client.cancel(task_id.clone()).await?;
+
+ if response.success {
+ output::success(&format!("Task {} cancelled", task_id));
+ } else {
+ output::warning(&format!(
+ "Task {} could not be cancelled (may already be finished, failed, or cancelled)",
+ task_id
+ ));
+ }
Ok(())
}
diff --git a/rust/robonix-cli/src/config.rs b/rust/robonix-cli/src/config.rs
index 4a23851..4257320 100644
--- a/rust/robonix-cli/src/config.rs
+++ b/rust/robonix-cli/src/config.rs
@@ -13,6 +13,12 @@ pub struct Config {
pub package_storage_path: PathBuf,
#[serde(skip_serializing_if = "Option::is_none")]
pub robonix_sdk_path: Option,
+ /// Node identifier (e.g. hostname) for capabilities registered by this CLI; shown in core web UI.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub node_id: Option,
+ /// Core web HTTP base URL (e.g. http://core-host:8080) for pushing capability logs when core has a viewer open.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub core_http_url: Option,
}
impl Config {
@@ -68,9 +74,20 @@ impl Config {
Self {
package_storage_path: default_path,
robonix_sdk_path: None,
+ node_id: None,
+ core_http_url: None,
}
}
+ /// Resolve node_id: config value or hostname.
+ pub fn effective_node_id(&self) -> String {
+ self.node_id.clone().unwrap_or_else(|| {
+ hostname::get()
+ .map(|h| h.to_string_lossy().into_owned())
+ .unwrap_or_else(|_| String::new())
+ })
+ }
+
pub fn ensure_storage_dir(&self) -> Result<()> {
// Check if path exists and is a directory (following symlinks)
if let Ok(metadata) = std::fs::metadata(&self.package_storage_path) {
diff --git a/rust/robonix-cli/src/daemon.rs b/rust/robonix-cli/src/daemon.rs
index 0ace6bf..1d929a5 100644
--- a/rust/robonix-cli/src/daemon.rs
+++ b/rust/robonix-cli/src/daemon.rs
@@ -5,14 +5,16 @@
use crate::daemon_client::{DaemonCommand, DaemonResponse, ProcessStatus};
use crate::daemon_ros2::DaemonRos2Clients;
+use crate::database::PackageDatabase;
use crate::process::ProcessManager;
+use crate::recipe_state::RecipeState;
use anyhow::{Context, Result};
use dirs;
-use log::{error, info, warn};
+use log::{error, info, trace, warn};
use robonix_core::ros_idl::primitive::RegisterPrimitiveRequest;
use robonix_core::ros_idl::service_registry::RegisterServiceRequest;
use robonix_core::ros_idl::skill::RegisterSkillRequest;
-use robonix_core::ros_idl::task::{SubmitTaskRequest, TaskDataRequest};
+use robonix_core::ros_idl::task::{CancelTaskRequest, SubmitTaskRequest, TaskDataRequest};
use std::path::PathBuf;
use std::sync::Arc;
use tokio::fs;
@@ -24,6 +26,14 @@ pub struct Daemon {
process_manager: Arc,
ros2_clients: Arc,
socket_path: PathBuf,
+ /// Core web HTTP base URL for pushing capability logs and node status
+ core_http_url: Option,
+ /// Node ID (e.g. hostname) for this CLI
+ node_id: String,
+ /// Log directory (package_storage_path/logs) for reading capability log files
+ log_dir: PathBuf,
+ /// Package storage path for loading database and recipe state (node status)
+ package_storage_path: PathBuf,
}
impl Daemon {
@@ -51,16 +61,69 @@ impl Daemon {
fs::remove_file(&socket_path).await?;
}
+ let log_dir = config.package_storage_path.join("logs");
+ let node_id = config.effective_node_id();
+ let package_storage_path = config.package_storage_path.clone();
+
Ok(Self {
process_manager,
ros2_clients,
socket_path,
+ core_http_url: config.core_http_url.clone(),
+ node_id,
+ log_dir,
+ package_storage_path,
})
}
pub async fn run(&self) -> Result<()> {
info!("Starting robonix daemon...");
+ // First: get core's listening IPs via ROS2 (daemon discovers core's network addresses)
+ {
+ let ros2 = self.ros2_clients.clone();
+ tokio::spawn(async move {
+ tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
+ match ros2.call_get_listening_ips().await {
+ Ok(ips) if !ips.is_empty() => {
+ info!("Core listening IPs (from ROS2): {}", ips.join(", "));
+ }
+ Ok(_) => {
+ trace!("Core get_listening_ips returned no IPs");
+ }
+ Err(e) => {
+ trace!("Could not get core listening IPs via ROS2: {}", e);
+ }
+ }
+ });
+ }
+
+ // Spawn background tasks when core_http_url is set: log push and node status
+ if let Some(ref base_url) = self.core_http_url {
+ let url = base_url.trim_end_matches('/').to_string();
+ let node_id = self.node_id.clone();
+ let log_dir = self.log_dir.clone();
+ tokio::spawn(async move {
+ Self::log_push_loop(url, node_id, log_dir).await;
+ });
+ info!("Log push to core enabled (core_http_url set)");
+
+ let url_status = base_url.trim_end_matches('/').to_string();
+ let node_id_status = self.node_id.clone();
+ let process_manager = self.process_manager.clone();
+ let package_storage_path = self.package_storage_path.clone();
+ tokio::spawn(async move {
+ Self::node_status_loop(
+ url_status,
+ node_id_status,
+ process_manager,
+ package_storage_path,
+ )
+ .await;
+ });
+ info!("Node status reporting to core enabled");
+ }
+
// Create Unix socket listener
let listener = UnixListener::bind(&self.socket_path)
.with_context(|| format!("Failed to bind socket: {}", self.socket_path.display()))?;
@@ -245,6 +308,17 @@ impl Daemon {
Err(e) => DaemonResponse::Error(format!("Invalid request: {}", e)),
}
}
+ DaemonCommand::CallCancelTask { request } => {
+ match serde_json::from_str::(&request) {
+ Ok(req) => match ros2_clients.call_cancel_task(req).await {
+ Ok(resp) => DaemonResponse::CancelTaskResponse {
+ response: serde_json::to_string(&resp)?,
+ },
+ Err(e) => DaemonResponse::Error(format!("Service call failed: {}", e)),
+ },
+ Err(e) => DaemonResponse::Error(format!("Invalid request: {}", e)),
+ }
+ }
};
// Send response
@@ -252,6 +326,346 @@ impl Daemon {
Ok(())
}
+ /// Background loop: when core has opened logs for this node, push local log file content.
+ async fn log_push_loop(base_url: String, node_id: String, log_dir: PathBuf) {
+ let client = match reqwest::Client::builder()
+ .timeout(std::time::Duration::from_secs(10))
+ .build()
+ {
+ Ok(c) => c,
+ Err(e) => {
+ warn!("log push: failed to create HTTP client: {}", e);
+ return;
+ }
+ };
+ let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(2));
+ loop {
+ interval.tick().await;
+ let subs_url = format!(
+ "{}/api/log-subscriptions?node_id={}",
+ base_url,
+ urlencoding::encode(&node_id)
+ );
+ let resp = match client.get(&subs_url).send().await {
+ Ok(r) => r,
+ Err(e) => {
+ trace!("log push: GET subscriptions failed: {}", e);
+ continue;
+ }
+ };
+ let capability_keys: Vec = match resp.json().await {
+ Ok(k) => k,
+ Err(_) => continue,
+ };
+ for capability_key in capability_keys {
+ // capability_key format: "provider/name" (e.g. demo_service_provider/srv::semantic_map)
+ let (provider, name) = match capability_key.split_once('/') {
+ Some(p) => p,
+ None => continue,
+ };
+ let clean_name = name.replace("::", "_").replace('.', "_");
+ let log_filename = format!("{}_{}.log", provider, clean_name);
+ let log_path = log_dir.join(&log_filename);
+ let content = match fs::read_to_string(&log_path).await {
+ Ok(c) => c,
+ Err(_) => continue,
+ };
+ let post_url = format!("{}/api/node-log", base_url);
+ let body = serde_json::json!({
+ "node_id": node_id,
+ "capability_key": capability_key,
+ "content": content
+ });
+ if client.post(&post_url).json(&body).send().await.is_err() {
+ trace!("log push: POST node-log failed for {}", capability_key);
+ }
+ }
+ }
+ }
+
+ /// Collect OS info: /etc/os-release key fields + kernel, arch, hostname.
+ fn collect_os_version() -> Option {
+ let mut lines: Vec = Vec::new();
+
+ if let Ok(content) = std::fs::read_to_string("/etc/os-release") {
+ let mut pretty = None;
+ let mut name = None;
+ let mut version = None;
+ let mut version_id = None;
+ let mut id_like = None;
+ for line in content.lines() {
+ if line.starts_with("PRETTY_NAME=") {
+ pretty = line
+ .strip_prefix("PRETTY_NAME=")
+ .map(|s| s.trim_matches('"').to_string());
+ } else if line.starts_with("NAME=") && name.is_none() {
+ name = line
+ .strip_prefix("NAME=")
+ .map(|s| s.trim_matches('"').to_string());
+ } else if line.starts_with("VERSION=") {
+ version = line
+ .strip_prefix("VERSION=")
+ .map(|s| s.trim_matches('"').to_string());
+ } else if line.starts_with("VERSION_ID=") {
+ version_id = line
+ .strip_prefix("VERSION_ID=")
+ .map(|s| s.trim_matches('"').to_string());
+ } else if line.starts_with("ID_LIKE=") {
+ id_like = line
+ .strip_prefix("ID_LIKE=")
+ .map(|s| s.trim_matches('"').to_string());
+ }
+ }
+ if let Some(p) = pretty {
+ lines.push(p);
+ } else if let (Some(ref n), Some(v)) =
+ (name.as_ref(), version.as_ref().or(version_id.as_ref()))
+ {
+ lines.push(format!("{} {}", n, v));
+ } else if let Some(n) = name {
+ lines.push(n);
+ }
+ if let Some(ref id) = version_id {
+ lines.push(format!("Version: {}", id));
+ }
+ if let Some(like) = id_like {
+ lines.push(format!("ID_LIKE: {}", like));
+ }
+ }
+
+ if let Ok(out) = std::process::Command::new("uname").args(["-r"]).output() {
+ if out.status.success() {
+ let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
+ if !s.is_empty() {
+ lines.push(format!("Kernel: {}", s));
+ }
+ }
+ }
+ if let Ok(out) = std::process::Command::new("uname").args(["-m"]).output() {
+ if out.status.success() {
+ let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
+ if !s.is_empty() {
+ lines.push(format!("Arch: {}", s));
+ }
+ }
+ }
+ if let Ok(h) = hostname::get() {
+ lines.push(format!("Hostname: {}", h.to_string_lossy()));
+ }
+
+ if lines.is_empty() {
+ None
+ } else {
+ Some(lines.join("\n"))
+ }
+ }
+
+ /// Collect CPU info: prefer lscpu (full output), else /proc/cpuinfo model + cores.
+ fn collect_cpu_info() -> Option {
+ if let Ok(out) = std::process::Command::new("lscpu").output() {
+ if out.status.success() {
+ let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
+ if !s.is_empty() {
+ return Some(s);
+ }
+ }
+ }
+
+ let content = std::fs::read_to_string("/proc/cpuinfo").ok()?;
+ let mut model = None;
+ let mut n_processor = 0u32;
+ for line in content.lines() {
+ if line.starts_with("model name") {
+ model = line.split(':').nth(1).map(|v| v.trim().to_string());
+ } else if line.starts_with("processor") {
+ n_processor += 1;
+ }
+ }
+ match (model, n_processor) {
+ (Some(m), n) if n > 0 => Some(format!("{}\nProcessors: {}", m, n)),
+ (Some(m), _) => Some(m),
+ _ => None,
+ }
+ }
+
+ /// Collect memory info: /proc/meminfo or free -h (for non-collapsible display).
+ fn collect_memory_info() -> Option {
+ if let Ok(content) = std::fs::read_to_string("/proc/meminfo") {
+ let mut mem_total = None;
+ let mut mem_avail = None;
+ let mut mem_free = None;
+ for line in content.lines() {
+ if line.starts_with("MemTotal:") {
+ mem_total = line.split_whitespace().nth(1).map(|s| s.to_string());
+ } else if line.starts_with("MemAvailable:") {
+ mem_avail = line.split_whitespace().nth(1).map(|s| s.to_string());
+ } else if line.starts_with("MemFree:") {
+ mem_free = line.split_whitespace().nth(1).map(|s| s.to_string());
+ }
+ }
+ let kb_to_gb = |s: Option| {
+ s.and_then(|v| v.parse::().ok())
+ .map(|kb| format!("{:.2} GiB", kb as f64 / 1_048_576.0))
+ };
+ if mem_total.is_some() || mem_avail.is_some() || mem_free.is_some() {
+ let mut mem_lines = vec!["Memory:".to_string()];
+ if let Some(t) = mem_total {
+ if let Some(g) = kb_to_gb(Some(t)) {
+ mem_lines.push(format!(" MemTotal: {}", g));
+ }
+ }
+ if let Some(a) = mem_avail {
+ if let Some(g) = kb_to_gb(Some(a)) {
+ mem_lines.push(format!(" MemAvailable: {}", g));
+ }
+ }
+ if let Some(f) = mem_free {
+ if let Some(g) = kb_to_gb(Some(f)) {
+ mem_lines.push(format!(" MemFree: {}", g));
+ }
+ }
+ return Some(mem_lines.join("\n"));
+ }
+ }
+ if let Ok(out) = std::process::Command::new("free").arg("-h").output() {
+ if out.status.success() {
+ let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
+ if !s.is_empty() {
+ return Some(s);
+ }
+ }
+ }
+ None
+ }
+
+ /// Collect disk info: df -h (for non-collapsible display).
+ fn collect_disk_info() -> Option {
+ if let Ok(out) = std::process::Command::new("df").args(["-h"]).output() {
+ if out.status.success() {
+ let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
+ if !s.is_empty() {
+ return Some(s);
+ }
+ }
+ }
+ None
+ }
+
+ /// Collect hardware: PCI, USB, lshw (collapsible block in UI).
+ fn collect_hw_info() -> Option {
+ let mut sections: Vec = Vec::new();
+
+ // PCI: lspci (full)
+ if let Ok(out) = std::process::Command::new("lspci").output() {
+ if out.status.success() {
+ let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
+ if !s.is_empty() {
+ sections.push(format!("PCI (lspci):\n{}", s));
+ }
+ }
+ }
+
+ // USB: lsusb (full)
+ if let Ok(out) = std::process::Command::new("lsusb").output() {
+ if out.status.success() {
+ let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
+ if !s.is_empty() {
+ sections.push(format!("USB (lsusb):\n{}", s));
+ }
+ }
+ }
+
+ // lshw -short (full)
+ if let Ok(output) = std::process::Command::new("lshw").args(["-short"]).output() {
+ if output.status.success() {
+ let s = String::from_utf8_lossy(&output.stdout).trim().to_string();
+ if !s.is_empty() {
+ sections.push(format!("Hardware (lshw -short):\n{}", s));
+ }
+ }
+ }
+
+ if sections.is_empty() {
+ None
+ } else {
+ Some(sections.join("\n\n"))
+ }
+ }
+
+ /// Background loop: periodically report node status (machine info + capability status) to core.
+ async fn node_status_loop(
+ base_url: String,
+ node_id: String,
+ process_manager: Arc,
+ package_storage_path: PathBuf,
+ ) {
+ let client = match reqwest::Client::builder()
+ .timeout(std::time::Duration::from_secs(10))
+ .build()
+ {
+ Ok(c) => c,
+ Err(e) => {
+ warn!("node status: failed to create HTTP client: {}", e);
+ return;
+ }
+ };
+ let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(15));
+ loop {
+ interval.tick().await;
+
+ let machine_info = serde_json::json!({
+ "os_version": Self::collect_os_version(),
+ "cpu_info": Self::collect_cpu_info(),
+ "memory_info": Self::collect_memory_info(),
+ "disk_info": Self::collect_disk_info(),
+ "hw_info": Self::collect_hw_info(),
+ });
+
+ let active_recipe = RecipeState::load(&package_storage_path)
+ .ok()
+ .flatten()
+ .map(|s| s.recipe.name.clone());
+ let packages: Vec = PackageDatabase::load(&package_storage_path)
+ .ok()
+ .map(|db| {
+ db.list_packages()
+ .into_iter()
+ .map(|p| serde_json::json!({ "name": p.name, "version": p.version }))
+ .collect()
+ })
+ .unwrap_or_default();
+ let running: Vec = process_manager
+ .get_running_processes()
+ .into_iter()
+ .map(|p| {
+ serde_json::json!({
+ "package_name": p.package_name,
+ "std_name": p.std_name,
+ "package_type": p.package_type,
+ "pid": p.pid,
+ })
+ })
+ .collect();
+
+ let capability_status = serde_json::json!({
+ "active_recipe": active_recipe,
+ "packages": packages,
+ "running": running,
+ });
+
+ let body = serde_json::json!({
+ "node_id": node_id,
+ "machine_info": machine_info,
+ "capability_status": capability_status,
+ });
+
+ let post_url = format!("{}/api/node-status", base_url);
+ if client.post(&post_url).json(&body).send().await.is_err() {
+ trace!("node status: POST failed");
+ }
+ }
+ }
+
async fn send_response(stream: &mut UnixStream, response: DaemonResponse) -> Result<()> {
let response_json = serde_json::to_string(&response)?;
let response_bytes = response_json.as_bytes();
diff --git a/rust/robonix-cli/src/daemon_client.rs b/rust/robonix-cli/src/daemon_client.rs
index a9df827..0ab6a20 100644
--- a/rust/robonix-cli/src/daemon_client.rs
+++ b/rust/robonix-cli/src/daemon_client.rs
@@ -42,6 +42,9 @@ pub enum DaemonCommand {
CallTaskData {
request: String, // JSON serialized TaskDataRequest
},
+ CallCancelTask {
+ request: String, // JSON serialized CancelTaskRequest
+ },
}
#[derive(Debug, Serialize, Deserialize)]
@@ -71,6 +74,9 @@ pub enum DaemonResponse {
TaskDataResponse {
response: String, // JSON serialized TaskDataResponse
},
+ CancelTaskResponse {
+ response: String, // JSON serialized CancelTaskResponse
+ },
}
#[derive(Debug, Serialize, Deserialize, Clone)]
diff --git a/rust/robonix-cli/src/daemon_ros2.rs b/rust/robonix-cli/src/daemon_ros2.rs
index 2685575..1aa8fac 100644
--- a/rust/robonix-cli/src/daemon_ros2.rs
+++ b/rust/robonix-cli/src/daemon_ros2.rs
@@ -4,17 +4,23 @@
// Manages persistent ROS2 node and service clients for daemon
use anyhow::Result;
+use robonix_core::ros_idl::get_listening_ips::{GetListeningIpsRequest, GetListeningIpsResponse};
use robonix_core::ros_idl::primitive::{RegisterPrimitiveRequest, RegisterPrimitiveResponse};
use robonix_core::ros_idl::service_registry::{RegisterServiceRequest, RegisterServiceResponse};
use robonix_core::ros_idl::skill::{RegisterSkillRequest, RegisterSkillResponse};
use robonix_core::ros_idl::task::{
- SubmitTaskRequest, SubmitTaskResponse, TaskDataRequest, TaskDataResponse,
+ CancelTaskRequest, CancelTaskResponse, SubmitTaskRequest, SubmitTaskResponse, TaskDataRequest,
+ TaskDataResponse,
};
use ros2_client::{
- service::AService, Context, Name, Node, NodeName, NodeOptions, ServiceMapping, ServiceTypeName,
+ Context, Name, Node, NodeName, NodeOptions, ServiceMapping, ServiceTypeName, service::AService,
+};
+use rustdds::{
+ Duration, QosPolicyBuilder,
+ policy::{self, Deadline, Lifespan},
};
-use rustdds::{policy, QosPolicyBuilder};
use std::sync::Arc;
+use std::sync::atomic::{AtomicBool, Ordering};
use tokio::sync::Mutex;
pub struct DaemonRos2Clients {
@@ -38,6 +44,14 @@ pub struct DaemonRos2Clients {
Arc>>>,
task_data_client:
Arc>>>,
+ task_cancel_client:
+ Arc>>>,
+ get_listening_ips_client: Arc<
+ Mutex<
+ ros2_client::service::Client>,
+ >,
+ >,
+ discovery_waited: Arc, // Track if we've already waited for service discovery
}
impl DaemonRos2Clients {
@@ -61,11 +75,20 @@ impl DaemonRos2Clients {
});
// Create service clients
+ // Match QoS settings with robonix-core server for compatibility
let service_qos = QosPolicyBuilder::new()
+ .history(policy::History::KeepLast { depth: 10 })
.reliability(policy::Reliability::Reliable {
- max_blocking_time: rustdds::Duration::from_millis(100),
+ max_blocking_time: Duration::from_millis(100),
+ })
+ .durability(policy::Durability::Volatile)
+ .deadline(Deadline(Duration::INFINITE))
+ .lifespan(Lifespan {
+ duration: Duration::INFINITE,
+ })
+ .liveliness(policy::Liveliness::Automatic {
+ lease_duration: Duration::INFINITE,
})
- .history(policy::History::KeepLast { depth: 1 })
.build();
let primitive_client = node
@@ -118,6 +141,26 @@ impl DaemonRos2Clients {
)
.map_err(|e| anyhow::anyhow!("Failed to create task_data client: {:?}", e))?;
+ let task_cancel_client = node
+ .create_client::>(
+ ServiceMapping::Enhanced,
+ &Name::new("/rbnx/task", "cancel").unwrap(),
+ &ServiceTypeName::new("robonix_sdk", "CancelTask"),
+ service_qos.clone(),
+ service_qos.clone(),
+ )
+ .map_err(|e| anyhow::anyhow!("Failed to create task_cancel client: {:?}", e))?;
+
+ let get_listening_ips_client = node
+ .create_client::>(
+ ServiceMapping::Enhanced,
+ &Name::new("/rbnx/core", "get_listening_ips").unwrap(),
+ &ServiceTypeName::new("robonix_sdk", "GetListeningIps"),
+ service_qos.clone(),
+ service_qos.clone(),
+ )
+ .map_err(|e| anyhow::anyhow!("Failed to create get_listening_ips client: {:?}", e))?;
+
Ok(Self {
_node: Arc::new(Mutex::new(node)),
primitive_client: Arc::new(Mutex::new(primitive_client)),
@@ -125,75 +168,221 @@ impl DaemonRos2Clients {
skill_client: Arc::new(Mutex::new(skill_client)),
submit_task_client: Arc::new(Mutex::new(submit_task_client)),
task_data_client: Arc::new(Mutex::new(task_data_client)),
+ task_cancel_client: Arc::new(Mutex::new(task_cancel_client)),
+ get_listening_ips_client: Arc::new(Mutex::new(get_listening_ips_client)),
+ discovery_waited: Arc::new(AtomicBool::new(false)),
})
}
- pub async fn call_register_primitive(
- &self,
- request: RegisterPrimitiveRequest,
- ) -> Result {
- let client = self.primitive_client.lock().await;
+ /// Get core's listening IPs via ROS2 (call this first to discover core's network addresses).
+ pub async fn call_get_listening_ips(&self) -> Result> {
+ self.wait_for_service_discovery().await;
+
+ let client = self.get_listening_ips_client.lock().await;
+ let req = GetListeningIpsRequest { _dummy: 0 };
let response = tokio::time::timeout(
tokio::time::Duration::from_secs(10),
- client.async_call_service(request),
+ client.async_call_service(req),
)
.await
- .map_err(|e| anyhow::anyhow!("Timeout: {}", e))?
- .map_err(|e| anyhow::anyhow!("Service call error: {:?}", e))?;
- Ok(response)
+ .map_err(|_| {
+ anyhow::anyhow!("Timeout: get_listening_ips timed out. Ensure robonix-core is running.")
+ })?
+ .map_err(|e| anyhow::anyhow!("get_listening_ips call error: {:?}", e))?;
+
+ let ips: Vec =
+ serde_json::from_str(&response.ips_json).unwrap_or_else(|_| Vec::new());
+ Ok(ips)
+ }
+
+ pub async fn call_register_primitive(
+ &self,
+ request: RegisterPrimitiveRequest,
+ ) -> Result {
+ // Wait for service discovery and retry if needed
+ self.wait_for_service_discovery().await;
+
+ // Retry logic: ROS2 service discovery can be slow
+ let max_retries = 3;
+ let mut last_error = None;
+
+ for attempt in 0..max_retries {
+ let client = self.primitive_client.lock().await;
+ let result = tokio::time::timeout(
+ tokio::time::Duration::from_secs(10),
+ client.async_call_service(request.clone()),
+ )
+ .await;
+
+ match result {
+ Ok(Ok(response)) => return Ok(response),
+ Ok(Err(e)) => {
+ last_error = Some(format!("Service call error: {:?}", e));
+ if attempt < max_retries - 1 {
+ tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
+ continue;
+ }
+ }
+ Err(_) => {
+ last_error = Some("Timeout: Service call timed out".to_string());
+ if attempt < max_retries - 1 {
+ tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
+ continue;
+ }
+ }
+ }
+ }
+
+ Err(anyhow::anyhow!(
+ "Service call to /rbnx/prm/register failed after {} attempts. {}. \
+ Please ensure robonix-core is running. Start it with: robonix-core",
+ max_retries,
+ last_error.unwrap_or_else(|| "Unknown error".to_string())
+ ))
}
pub async fn call_register_service(
&self,
request: RegisterServiceRequest,
) -> Result {
- let client = self.service_client.lock().await;
- let response = tokio::time::timeout(
- tokio::time::Duration::from_secs(10),
- client.async_call_service(request),
- )
- .await
- .map_err(|e| anyhow::anyhow!("Timeout: {}", e))?
- .map_err(|e| anyhow::anyhow!("Service call error: {:?}", e))?;
- Ok(response)
+ // Wait for service discovery and retry if needed
+ self.wait_for_service_discovery().await;
+
+ // Retry logic: ROS2 service discovery can be slow
+ let max_retries = 3;
+ let mut last_error = None;
+
+ for attempt in 0..max_retries {
+ let client = self.service_client.lock().await;
+ let result = tokio::time::timeout(
+ tokio::time::Duration::from_secs(10),
+ client.async_call_service(request.clone()),
+ )
+ .await;
+
+ match result {
+ Ok(Ok(response)) => return Ok(response),
+ Ok(Err(e)) => {
+ last_error = Some(format!("Service call error: {:?}", e));
+ // If it's a service discovery error, wait and retry
+ if attempt < max_retries - 1 {
+ tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
+ continue;
+ }
+ }
+ Err(_) => {
+ last_error = Some("Timeout: Service call timed out".to_string());
+ // If timeout, wait and retry (might be service discovery issue)
+ if attempt < max_retries - 1 {
+ tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
+ continue;
+ }
+ }
+ }
+ }
+
+ Err(anyhow::anyhow!(
+ "Service call to /rbnx/srv/register failed after {} attempts. {}. \
+ Please ensure robonix-core is running. Start it with: robonix-core",
+ max_retries,
+ last_error.unwrap_or_else(|| "Unknown error".to_string())
+ ))
}
pub async fn call_register_skill(
&self,
request: RegisterSkillRequest,
) -> Result {
+ // Wait for service discovery before calling
+ self.wait_for_service_discovery().await;
+
let client = self.skill_client.lock().await;
let response = tokio::time::timeout(
tokio::time::Duration::from_secs(10),
client.async_call_service(request),
)
.await
- .map_err(|e| anyhow::anyhow!("Timeout: {}", e))?
+ .map_err(|_| {
+ anyhow::anyhow!(
+ "Timeout: Service call to /rbnx/skl/register timed out after 10 seconds. \
+ Please ensure robonix-core is running. Start it with: robonix-core"
+ )
+ })?
.map_err(|e| anyhow::anyhow!("Service call error: {:?}", e))?;
Ok(response)
}
pub async fn call_submit_task(&self, request: SubmitTaskRequest) -> Result {
+ // Wait for service discovery before calling
+ self.wait_for_service_discovery().await;
+
let client = self.submit_task_client.lock().await;
let response = tokio::time::timeout(
tokio::time::Duration::from_secs(30),
client.async_call_service(request),
)
.await
- .map_err(|e| anyhow::anyhow!("Timeout: {}", e))?
+ .map_err(|_| {
+ anyhow::anyhow!(
+ "Timeout: Service call to /rbnx/task/submit timed out after 30 seconds. \
+ Please ensure robonix-core is running. Start it with: robonix-core"
+ )
+ })?
.map_err(|e| anyhow::anyhow!("Service call error: {:?}", e))?;
Ok(response)
}
pub async fn call_task_data(&self, request: TaskDataRequest) -> Result {
+ // Wait for service discovery before calling
+ self.wait_for_service_discovery().await;
+
let client = self.task_data_client.lock().await;
let response = tokio::time::timeout(
tokio::time::Duration::from_secs(10),
client.async_call_service(request),
)
.await
- .map_err(|e| anyhow::anyhow!("Timeout: {}", e))?
+ .map_err(|_| {
+ anyhow::anyhow!(
+ "Timeout: Service call to /rbnx/task/data timed out after 10 seconds. \
+ Please ensure robonix-core is running. Start it with: robonix-core"
+ )
+ })?
+ .map_err(|e| anyhow::anyhow!("Service call error: {:?}", e))?;
+ Ok(response)
+ }
+
+ pub async fn call_cancel_task(&self, request: CancelTaskRequest) -> Result {
+ self.wait_for_service_discovery().await;
+
+ let client = self.task_cancel_client.lock().await;
+ let response = tokio::time::timeout(
+ tokio::time::Duration::from_secs(10),
+ client.async_call_service(request),
+ )
+ .await
+ .map_err(|_| {
+ anyhow::anyhow!(
+ "Timeout: Service call to /rbnx/task/cancel timed out after 10 seconds. \
+ Please ensure robonix-core is running. Start it with: robonix-core"
+ )
+ })?
.map_err(|e| anyhow::anyhow!("Service call error: {:?}", e))?;
Ok(response)
}
+
+ /// Wait for ROS2 service discovery to complete
+ /// This gives time for the ROS2 DDS discovery process to find services
+ /// Only waits once per DaemonRos2Clients instance (first call)
+ async fn wait_for_service_discovery(&self) {
+ // Only wait on the first call - subsequent calls don't need to wait
+ // as service discovery is a one-time process
+ if !self.discovery_waited.swap(true, Ordering::Relaxed) {
+ // ROS2 service discovery typically takes 1-5 seconds, especially when
+ // daemon starts before robonix-core or when they start simultaneously.
+ // We wait a reasonable time to allow discovery to complete.
+ // The spinner is already running in the background to help with discovery.
+ tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
+ }
+ }
}
diff --git a/rust/robonix-cli/src/process.rs b/rust/robonix-cli/src/process.rs
index a835dd8..092b465 100644
--- a/rust/robonix-cli/src/process.rs
+++ b/rust/robonix-cli/src/process.rs
@@ -235,28 +235,18 @@ impl ProcessManager {
}
}
- // Resolve script path
- let script_path = package_path.join(start_script);
- if !script_path.exists() {
- anyhow::bail!("Start script not found: {}", script_path.display());
+ // start_script is run as a full shell command (any command); cwd is package_path
+ let start_script = start_script.trim();
+ if start_script.is_empty() {
+ anyhow::bail!("start_script is empty");
}
- // Make script executable
- #[cfg(unix)]
- {
- use std::os::unix::fs::PermissionsExt;
- let mut perms = std::fs::metadata(&script_path)?.permissions();
- perms.set_mode(0o755);
- std::fs::set_permissions(&script_path, perms)?;
- }
+ // Create log file path with simplified naming
+ // Format: {package_name}_{name}.log
+ // e.g., tiago_demo_package_camera_capture.log
+ let clean_name = std_name.replace("::", "_").replace(".", "_");
- // Create log file path
- let log_filename = format!(
- "{}_{}_{}.log",
- package_name,
- package_type,
- std_name.replace("::", "_")
- );
+ let log_filename = format!("{}_{}.log", package_name, clean_name);
let log_file = self.log_dir.join(&log_filename);
// Open log file for writing
@@ -279,15 +269,11 @@ impl ProcessManager {
log_writer.write_all(header.as_bytes()).await?;
log_writer.flush().await?;
- // Start the process
- log::info!(
- "Starting process: {} (script: {})",
- key,
- script_path.display()
- );
+ // Start the process: run start_script as a shell command (sh -c "...")
+ log::info!("Starting process: {} (command: {})", key, start_script);
- // Use tokio::process::Command for async I/O
- let mut cmd = Command::new(&script_path);
+ let mut cmd = Command::new("sh");
+ cmd.arg("-c").arg(start_script);
cmd.current_dir(package_path);
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
@@ -296,6 +282,9 @@ impl ProcessManager {
// This ensures logs are written immediately
cmd.env("PYTHONUNBUFFERED", "1");
+ // Force FastDDS for all started processes
+ cmd.env("RMW_IMPLEMENTATION", "rmw_fastrtps_cpp");
+
// Set ROBONIX_SDK_PATH from config or environment variable
if std::env::var("ROBONIX_SDK_PATH").is_err() {
if let Some(config_path) = robonix_sdk_path {
@@ -323,7 +312,7 @@ impl ProcessManager {
let mut child = cmd
.spawn()
- .with_context(|| format!("Failed to start script: {}", script_path.display()))?;
+ .with_context(|| format!("Failed to start command: {}", start_script))?;
// Spawn tasks to capture output and write to log
// Use separate file handles for stdout and stderr to avoid synchronization issues
@@ -540,7 +529,7 @@ impl ProcessManager {
/// Kill a process group (more efficient than killing individual processes)
#[cfg(unix)]
fn kill_process_tree(&self, pid: u32) -> Result<()> {
- use nix::sys::signal::{kill, killpg, Signal};
+ use nix::sys::signal::{Signal, kill, killpg};
use nix::unistd::Pid;
use std::io::{BufRead, BufReader};
use std::process::Command as SyncCommand;
@@ -856,7 +845,7 @@ impl ProcessManager {
// Fallback: try to kill just the main process
#[cfg(unix)]
{
- use nix::sys::signal::{kill, Signal};
+ use nix::sys::signal::{Signal, kill};
use nix::unistd::Pid;
let pid = Pid::from_raw(process_info.pid as i32);
let _ = kill(pid, Signal::SIGTERM);
diff --git a/rust/robonix-cli/src/register.rs b/rust/robonix-cli/src/register.rs
index 5fd4d3e..5130903 100644
--- a/rust/robonix-cli/src/register.rs
+++ b/rust/robonix-cli/src/register.rs
@@ -98,9 +98,23 @@ impl PackageRegistrar {
}
}
- // Save recipe state
+ // Save recipe state with absolute path (realpath)
+ let abs_recipe_path = recipe_path.canonicalize().unwrap_or_else(|_| {
+ // If canonicalize fails (e.g., file doesn't exist), try to make it absolute
+ // by joining with current directory
+ std::env::current_dir()
+ .ok()
+ .and_then(|cwd| {
+ if recipe_path.is_absolute() {
+ Some(recipe_path.clone())
+ } else {
+ Some(cwd.join(recipe_path))
+ }
+ })
+ .unwrap_or_else(|| recipe_path.clone())
+ });
let recipe_state = RecipeState {
- recipe_path: recipe_path.clone(),
+ recipe_path: abs_recipe_path,
recipe: recipe.clone(),
registered_at: chrono::Utc::now().to_rfc3339(),
};
@@ -150,6 +164,7 @@ impl PackageRegistrar {
metadata: metadata_str.to_string(),
provider: provider.clone(),
version,
+ node_id: self.config.effective_node_id(),
};
self.call_primitive_register_service(request).await?;
@@ -198,6 +213,7 @@ impl PackageRegistrar {
metadata: metadata_str.to_string(),
provider: provider.clone(),
version,
+ node_id: self.config.effective_node_id(),
};
self.call_service_register_service(request).await?;
@@ -214,16 +230,10 @@ impl PackageRegistrar {
package_path: &PathBuf,
skill: &Value,
) -> Result<()> {
- let name_raw = skill["name"]
+ let name = skill["name"]
.as_str()
.ok_or_else(|| anyhow::anyhow!("Skill name not found"))?
.to_string();
- // Automatically add 'skl::' prefix if not present
- let name = if name_raw.starts_with("skl::") {
- name_raw
- } else {
- format!("skl::{}", name_raw)
- };
let start_topic = skill["start_topic"]
.as_str()
@@ -315,6 +325,7 @@ impl PackageRegistrar {
metadata: metadata_str.to_string(),
provider: provider.clone(),
version,
+ node_id: self.config.effective_node_id(),
};
let response = self.call_skill_register_service(request).await?;
diff --git a/rust/robonix-cli/src/task.rs b/rust/robonix-cli/src/task.rs
index 85543d3..00fff78 100644
--- a/rust/robonix-cli/src/task.rs
+++ b/rust/robonix-cli/src/task.rs
@@ -6,7 +6,8 @@
use crate::config::Config;
use anyhow::Result;
use robonix_core::ros_idl::task::{
- SubmitTaskRequest, SubmitTaskResponse, TaskDataRequest, TaskDataResponse,
+ CancelTaskRequest, CancelTaskResponse, SubmitTaskRequest, SubmitTaskResponse, TaskDataRequest,
+ TaskDataResponse,
};
pub struct TaskClient {
@@ -74,4 +75,28 @@ impl TaskClient {
_ => anyhow::bail!("Unexpected response type"),
}
}
+
+ pub async fn cancel(&self, task_id: String) -> Result {
+ use crate::daemon_client::{DaemonClient, DaemonCommand, DaemonResponse};
+
+ let daemon_client = DaemonClient::new()?;
+ daemon_client.ensure_daemon_running().await?;
+
+ let request = CancelTaskRequest { task_id };
+ let request_json = serde_json::to_string(&request)?;
+ let response = daemon_client
+ .send_command(DaemonCommand::CallCancelTask {
+ request: request_json,
+ })
+ .await?;
+
+ match response {
+ DaemonResponse::CancelTaskResponse { response } => {
+ let resp: CancelTaskResponse = serde_json::from_str(&response)?;
+ Ok(resp)
+ }
+ DaemonResponse::Error(e) => anyhow::bail!("Daemon error: {}", e),
+ _ => anyhow::bail!("Unexpected response type"),
+ }
+ }
}
diff --git a/rust/robonix-core/Cargo.lock b/rust/robonix-core/Cargo.lock
index 306d34c..095fae9 100644
--- a/rust/robonix-core/Cargo.lock
+++ b/rust/robonix-core/Cargo.lock
@@ -2,6 +2,12 @@
# It is not intended for manual editing.
version = 4
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
[[package]]
name = "aho-corasick"
version = "1.1.4"
@@ -11,6 +17,24 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "aligned"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685"
+dependencies = [
+ "as-slice",
+]
+
+[[package]]
+name = "aligned-vec"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b"
+dependencies = [
+ "equator",
+]
+
[[package]]
name = "android_system_properties"
version = "0.1.5"
@@ -79,6 +103,44 @@ dependencies = [
"windows-sys 0.60.2",
]
+[[package]]
+name = "anyhow"
+version = "1.0.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
+
+[[package]]
+name = "arbitrary"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
+
+[[package]]
+name = "arg_enum_proc_macro"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.108",
+]
+
+[[package]]
+name = "arrayvec"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
+
+[[package]]
+name = "as-slice"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516"
+dependencies = [
+ "stable_deref_trait",
+]
+
[[package]]
name = "async-channel"
version = "2.5.0"
@@ -192,12 +254,60 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "async-stream"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
+dependencies = [
+ "async-stream-impl",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-stream-impl"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.108",
+]
+
[[package]]
name = "async-task"
version = "4.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
+[[package]]
+name = "async-trait"
+version = "0.1.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.108",
+]
+
+[[package]]
+name = "atomic"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba"
+
+[[package]]
+name = "atomic"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340"
+dependencies = [
+ "bytemuck",
+]
+
[[package]]
name = "atomic-waker"
version = "1.1.2"
@@ -210,18 +320,95 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+[[package]]
+name = "av-scenechange"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394"
+dependencies = [
+ "aligned",
+ "anyhow",
+ "arg_enum_proc_macro",
+ "arrayvec",
+ "log",
+ "num-rational",
+ "num-traits",
+ "pastey",
+ "rayon",
+ "thiserror 2.0.17",
+ "v_frame",
+ "y4m",
+]
+
+[[package]]
+name = "av1-grain"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8"
+dependencies = [
+ "anyhow",
+ "arrayvec",
+ "log",
+ "nom",
+ "num-rational",
+ "v_frame",
+]
+
+[[package]]
+name = "avif-serialize"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47c8fbc0f831f4519fe8b810b6a7a91410ec83031b8233f730a0480029f6a23f"
+dependencies = [
+ "arrayvec",
+]
+
+[[package]]
+name = "aws-lc-rs"
+version = "1.15.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b7b6141e96a8c160799cc2d5adecd5cbbe5054cb8c7c4af53da0f83bb7ad256"
+dependencies = [
+ "aws-lc-sys",
+ "zeroize",
+]
+
+[[package]]
+name = "aws-lc-sys"
+version = "0.37.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c34dda4df7017c8db52132f0f8a2e0f8161649d15723ed63fc00c82d0f2081a"
+dependencies = [
+ "cc",
+ "cmake",
+ "dunce",
+ "fs_extra",
+]
+
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+[[package]]
+name = "binascii"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72"
+
[[package]]
name = "bit-vec"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
+[[package]]
+name = "bit_field"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6"
+
[[package]]
name = "bitflags"
version = "1.3.2"
@@ -234,6 +421,15 @@ version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
+[[package]]
+name = "bitstream-io"
+version = "4.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757"
+dependencies = [
+ "core2",
+]
+
[[package]]
name = "blocking"
version = "1.6.2"
@@ -258,18 +454,36 @@ dependencies = [
"serde",
]
+[[package]]
+name = "built"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64"
+
[[package]]
name = "bumpalo"
version = "3.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
+[[package]]
+name = "bytemuck"
+version = "1.24.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4"
+
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+[[package]]
+name = "byteorder-lite"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
+
[[package]]
name = "bytes"
version = "1.10.1"
@@ -281,11 +495,13 @@ dependencies = [
[[package]]
name = "cc"
-version = "1.2.43"
+version = "1.2.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "739eb0f94557554b3ca9a86d2d37bebd49c5e6d0c1d2bda35ba5bdac830befc2"
+checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583"
dependencies = [
"find-msvc-tools",
+ "jobserver",
+ "libc",
"shlex",
]
@@ -324,6 +540,12 @@ dependencies = [
"syn 1.0.109",
]
+[[package]]
+name = "cesu8"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
+
[[package]]
name = "cfg-if"
version = "0.1.10"
@@ -336,6 +558,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
[[package]]
name = "chrono"
version = "0.4.42"
@@ -377,12 +605,37 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
+[[package]]
+name = "cmake"
+version = "0.1.57"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "color_quant"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
+
[[package]]
name = "colorchoice"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
+[[package]]
+name = "combine"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
+dependencies = [
+ "bytes",
+ "memchr",
+]
+
[[package]]
name = "concurrent-queue"
version = "2.5.0"
@@ -392,6 +645,17 @@ dependencies = [
"crossbeam-utils",
]
+[[package]]
+name = "cookie"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
+dependencies = [
+ "percent-encoding",
+ "time",
+ "version_check",
+]
+
[[package]]
name = "core-foundation"
version = "0.9.4"
@@ -402,18 +666,134 @@ dependencies = [
"libc",
]
+[[package]]
+name = "core-foundation"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+[[package]]
+name = "core2"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if 1.0.4",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+[[package]]
+name = "crunchy"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
+
+[[package]]
+name = "deranged"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587"
+dependencies = [
+ "powerfmt",
+]
+
+[[package]]
+name = "devise"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1d90b0c4c777a2cad215e3c7be59ac7c15adf45cf76317009b7d096d46f651d"
+dependencies = [
+ "devise_codegen",
+ "devise_core",
+]
+
+[[package]]
+name = "devise_codegen"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71b28680d8be17a570a2334922518be6adc3f58ecc880cbb404eaeb8624fd867"
+dependencies = [
+ "devise_core",
+ "quote",
+]
+
+[[package]]
+name = "devise_core"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b035a542cf7abf01f2e3c4d5a7acbaebfefe120ae4efc7bde3df98186e4b8af7"
+dependencies = [
+ "bitflags 2.10.0",
+ "proc-macro2",
+ "proc-macro2-diagnostics",
+ "quote",
+ "syn 2.0.108",
+]
+
+[[package]]
+name = "dirs"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "displaydoc"
version = "0.2.5"
@@ -425,6 +805,12 @@ dependencies = [
"syn 2.0.108",
]
+[[package]]
+name = "dunce"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
+
[[package]]
name = "either"
version = "1.15.0"
@@ -484,6 +870,26 @@ dependencies = [
"log",
]
+[[package]]
+name = "equator"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc"
+dependencies = [
+ "equator-macro",
+]
+
+[[package]]
+name = "equator-macro"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.108",
+]
+
[[package]]
name = "equivalent"
version = "1.0.2"
@@ -521,6 +927,21 @@ dependencies = [
"pin-project-lite",
]
+[[package]]
+name = "exr"
+version = "1.74.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be"
+dependencies = [
+ "bit_field",
+ "half",
+ "lebe",
+ "miniz_oxide",
+ "rayon-core",
+ "smallvec",
+ "zune-inflate",
+]
+
[[package]]
name = "fastrand"
version = "2.3.0"
@@ -528,31 +949,69 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
-name = "find-msvc-tools"
-version = "0.1.4"
+name = "fax"
+version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127"
+checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab"
+dependencies = [
+ "fax_derive",
+]
[[package]]
-name = "fnv"
-version = "1.0.7"
+name = "fax_derive"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.108",
+]
[[package]]
-name = "foreign-types"
-version = "0.3.2"
+name = "fdeflate"
+version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
dependencies = [
- "foreign-types-shared",
+ "simd-adler32",
]
[[package]]
-name = "foreign-types-shared"
-version = "0.1.1"
+name = "figment"
+version = "0.10.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3"
+dependencies = [
+ "atomic 0.6.1",
+ "pear",
+ "serde",
+ "toml",
+ "uncased",
+ "version_check",
+]
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db"
+
+[[package]]
+name = "flate2"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "form_urlencoded"
@@ -563,6 +1022,12 @@ dependencies = [
"percent-encoding",
]
+[[package]]
+name = "fs_extra"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
+
[[package]]
name = "fuchsia-zircon"
version = "0.3.3"
@@ -681,6 +1146,19 @@ dependencies = [
"slab",
]
+[[package]]
+name = "generator"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e"
+dependencies = [
+ "cc",
+ "libc",
+ "log",
+ "rustversion",
+ "windows",
+]
+
[[package]]
name = "getrandom"
version = "0.2.16"
@@ -688,8 +1166,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [
"cfg-if 1.0.4",
+ "js-sys",
"libc",
"wasi",
+ "wasm-bindgen",
]
[[package]]
@@ -699,9 +1179,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if 1.0.4",
+ "js-sys",
"libc",
"r-efi",
"wasip2",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "gif"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e"
+dependencies = [
+ "color_quant",
+ "weezl",
]
[[package]]
@@ -710,6 +1202,25 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
+[[package]]
+name = "h2"
+version = "0.3.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d"
+dependencies = [
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http 0.2.12",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
[[package]]
name = "h2"
version = "0.4.12"
@@ -721,7 +1232,7 @@ dependencies = [
"fnv",
"futures-core",
"futures-sink",
- "http",
+ "http 1.3.1",
"indexmap",
"slab",
"tokio",
@@ -729,6 +1240,17 @@ dependencies = [
"tracing",
]
+[[package]]
+name = "half"
+version = "2.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
+dependencies = [
+ "cfg-if 1.0.4",
+ "crunchy",
+ "zerocopy",
+]
+
[[package]]
name = "hashbrown"
version = "0.16.0"
@@ -741,6 +1263,17 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
+[[package]]
+name = "http"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
[[package]]
name = "http"
version = "1.3.1"
@@ -752,6 +1285,17 @@ dependencies = [
"itoa",
]
+[[package]]
+name = "http-body"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
+dependencies = [
+ "bytes",
+ "http 0.2.12",
+ "pin-project-lite",
+]
+
[[package]]
name = "http-body"
version = "1.0.1"
@@ -759,7 +1303,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
dependencies = [
"bytes",
- "http",
+ "http 1.3.1",
]
[[package]]
@@ -770,8 +1314,8 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
dependencies = [
"bytes",
"futures-core",
- "http",
- "http-body",
+ "http 1.3.1",
+ "http-body 1.0.1",
"pin-project-lite",
]
@@ -781,6 +1325,36 @@ version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "hyper"
+version = "0.14.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2 0.3.27",
+ "http 0.2.12",
+ "http-body 0.4.6",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "socket2 0.5.10",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "want",
+]
+
[[package]]
name = "hyper"
version = "1.8.1"
@@ -791,9 +1365,9 @@ dependencies = [
"bytes",
"futures-channel",
"futures-core",
- "h2",
- "http",
- "http-body",
+ "h2 0.4.12",
+ "http 1.3.1",
+ "http-body 1.0.1",
"httparse",
"itoa",
"pin-project-lite",
@@ -809,8 +1383,8 @@ version = "0.27.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
dependencies = [
- "http",
- "hyper",
+ "http 1.3.1",
+ "hyper 1.8.1",
"hyper-util",
"rustls",
"rustls-pki-types",
@@ -819,22 +1393,6 @@ dependencies = [
"tower-service",
]
-[[package]]
-name = "hyper-tls"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
-dependencies = [
- "bytes",
- "http-body-util",
- "hyper",
- "hyper-util",
- "native-tls",
- "tokio",
- "tokio-native-tls",
- "tower-service",
-]
-
[[package]]
name = "hyper-util"
version = "0.1.18"
@@ -846,9 +1404,9 @@ dependencies = [
"futures-channel",
"futures-core",
"futures-util",
- "http",
- "http-body",
- "hyper",
+ "http 1.3.1",
+ "http-body 1.0.1",
+ "hyper 1.8.1",
"ipnet",
"libc",
"percent-encoding",
@@ -997,6 +1555,46 @@ dependencies = [
"windows-sys 0.59.0",
]
+[[package]]
+name = "image"
+version = "0.25.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a"
+dependencies = [
+ "bytemuck",
+ "byteorder-lite",
+ "color_quant",
+ "exr",
+ "gif",
+ "image-webp",
+ "moxcms",
+ "num-traits",
+ "png",
+ "qoi",
+ "ravif",
+ "rayon",
+ "rgb",
+ "tiff",
+ "zune-core 0.5.0",
+ "zune-jpeg 0.5.8",
+]
+
+[[package]]
+name = "image-webp"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3"
+dependencies = [
+ "byteorder-lite",
+ "quick-error",
+]
+
+[[package]]
+name = "imgref"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8"
+
[[package]]
name = "indexmap"
version = "2.12.0"
@@ -1005,6 +1603,25 @@ checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f"
dependencies = [
"equivalent",
"hashbrown",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "inlinable_string"
+version = "0.1.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb"
+
+[[package]]
+name = "interpolate_name"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.108",
]
[[package]]
@@ -1057,6 +1674,17 @@ dependencies = [
"serde",
]
+[[package]]
+name = "is-terminal"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
@@ -1102,6 +1730,38 @@ dependencies = [
"syn 2.0.108",
]
+[[package]]
+name = "jni"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
+dependencies = [
+ "cesu8",
+ "cfg-if 1.0.4",
+ "combine",
+ "jni-sys",
+ "log",
+ "thiserror 1.0.69",
+ "walkdir",
+ "windows-sys 0.45.0",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
+
+[[package]]
+name = "jobserver"
+version = "0.1.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
+dependencies = [
+ "getrandom 0.3.4",
+ "libc",
+]
+
[[package]]
name = "js-sys"
version = "0.3.82"
@@ -1134,12 +1794,38 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
+[[package]]
+name = "lebe"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8"
+
[[package]]
name = "libc"
version = "0.2.177"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
+[[package]]
+name = "libfuzzer-sys"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404"
+dependencies = [
+ "arbitrary",
+ "cc",
+]
+
+[[package]]
+name = "libredox"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
+dependencies = [
+ "bitflags 2.10.0",
+ "libc",
+]
+
[[package]]
name = "linux-raw-sys"
version = "0.11.0"
@@ -1179,6 +1865,55 @@ version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
+[[package]]
+name = "loom"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5"
+dependencies = [
+ "cfg-if 1.0.4",
+ "generator",
+ "scoped-tls",
+ "serde",
+ "serde_json",
+ "tracing",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "loop9"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062"
+dependencies = [
+ "imgref",
+]
+
+[[package]]
+name = "lru-slab"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
+
+[[package]]
+name = "matchers"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
+dependencies = [
+ "regex-automata",
+]
+
+[[package]]
+name = "maybe-rayon"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519"
+dependencies = [
+ "cfg-if 1.0.4",
+ "rayon",
+]
+
[[package]]
name = "md5"
version = "0.8.0"
@@ -1206,6 +1941,16 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
[[package]]
name = "mio"
version = "0.6.23"
@@ -1273,33 +2018,45 @@ dependencies = [
]
[[package]]
-name = "native-tls"
-version = "0.2.14"
+name = "moxcms"
+version = "0.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
+checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97"
dependencies = [
- "libc",
- "log",
- "openssl",
- "openssl-probe",
- "openssl-sys",
- "schannel",
- "security-framework",
- "security-framework-sys",
- "tempfile",
+ "num-traits",
+ "pxfm",
]
[[package]]
-name = "neli"
-version = "0.6.5"
+name = "multer"
+version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93062a0dce6da2517ea35f301dfc88184ce18d3601ec786a727a87bf535deca9"
+checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b"
dependencies = [
- "byteorder",
- "libc",
- "log",
- "neli-proc-macros",
-]
+ "bytes",
+ "encoding_rs",
+ "futures-util",
+ "http 1.3.1",
+ "httparse",
+ "memchr",
+ "mime",
+ "spin",
+ "tokio",
+ "tokio-util",
+ "version_check",
+]
+
+[[package]]
+name = "neli"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93062a0dce6da2517ea35f301dfc88184ce18d3601ec786a727a87bf535deca9"
+dependencies = [
+ "byteorder",
+ "libc",
+ "log",
+ "neli-proc-macros",
+]
[[package]]
name = "neli-proc-macros"
@@ -1325,6 +2082,12 @@ dependencies = [
"winapi 0.3.9",
]
+[[package]]
+name = "new_debug_unreachable"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
+
[[package]]
name = "no-std-net"
version = "0.6.0"
@@ -1340,6 +2103,37 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "noop_proc_macro"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.50.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+
[[package]]
name = "num-derive"
version = "0.4.2"
@@ -1351,6 +2145,26 @@ dependencies = [
"syn 2.0.108",
]
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-rational"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
+dependencies = [
+ "num-bigint",
+ "num-integer",
+ "num-traits",
+]
+
[[package]]
name = "num-traits"
version = "0.2.19"
@@ -1360,6 +2174,16 @@ dependencies = [
"autocfg",
]
+[[package]]
+name = "num_cpus"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
[[package]]
name = "once_cell"
version = "1.21.3"
@@ -1372,49 +2196,17 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
-[[package]]
-name = "openssl"
-version = "0.10.75"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
-dependencies = [
- "bitflags 2.10.0",
- "cfg-if 1.0.4",
- "foreign-types",
- "libc",
- "once_cell",
- "openssl-macros",
- "openssl-sys",
-]
-
-[[package]]
-name = "openssl-macros"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.108",
-]
-
[[package]]
name = "openssl-probe"
-version = "0.1.6"
+version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
+checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
[[package]]
-name = "openssl-sys"
-version = "0.9.111"
+name = "option-ext"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321"
-dependencies = [
- "cc",
- "libc",
- "pkg-config",
- "vcpkg",
-]
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "parking"
@@ -1451,6 +2243,35 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+[[package]]
+name = "pastey"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
+
+[[package]]
+name = "pear"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467"
+dependencies = [
+ "inlinable_string",
+ "pear_codegen",
+ "yansi",
+]
+
+[[package]]
+name = "pear_codegen"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147"
+dependencies = [
+ "proc-macro2",
+ "proc-macro2-diagnostics",
+ "quote",
+ "syn 2.0.108",
+]
+
[[package]]
name = "percent-encoding"
version = "2.3.2"
@@ -1480,12 +2301,6 @@ dependencies = [
"futures-io",
]
-[[package]]
-name = "pkg-config"
-version = "0.3.32"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
-
[[package]]
name = "pnet"
version = "0.35.0"
@@ -1577,6 +2392,19 @@ dependencies = [
"pnet_sys",
]
+[[package]]
+name = "png"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0"
+dependencies = [
+ "bitflags 2.10.0",
+ "crc32fast",
+ "fdeflate",
+ "flate2",
+ "miniz_oxide",
+]
+
[[package]]
name = "polling"
version = "3.11.0"
@@ -1615,6 +2443,12 @@ dependencies = [
"zerovec",
]
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
[[package]]
name = "ppv-lite86"
version = "0.2.21"
@@ -1633,6 +2467,118 @@ dependencies = [
"unicode-ident",
]
+[[package]]
+name = "proc-macro2-diagnostics"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.108",
+ "version_check",
+ "yansi",
+]
+
+[[package]]
+name = "profiling"
+version = "1.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773"
+dependencies = [
+ "profiling-procmacros",
+]
+
+[[package]]
+name = "profiling-procmacros"
+version = "1.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b"
+dependencies = [
+ "quote",
+ "syn 2.0.108",
+]
+
+[[package]]
+name = "pxfm"
+version = "0.1.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "qoi"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001"
+dependencies = [
+ "bytemuck",
+]
+
+[[package]]
+name = "quick-error"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
+
+[[package]]
+name = "quinn"
+version = "0.11.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
+dependencies = [
+ "bytes",
+ "cfg_aliases",
+ "pin-project-lite",
+ "quinn-proto",
+ "quinn-udp",
+ "rustc-hash",
+ "rustls",
+ "socket2 0.6.1",
+ "thiserror 2.0.17",
+ "tokio",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-proto"
+version = "0.11.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31"
+dependencies = [
+ "aws-lc-rs",
+ "bytes",
+ "getrandom 0.3.4",
+ "lru-slab",
+ "rand 0.9.2",
+ "ring",
+ "rustc-hash",
+ "rustls",
+ "rustls-pki-types",
+ "slab",
+ "thiserror 2.0.17",
+ "tinyvec",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-udp"
+version = "0.5.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
+dependencies = [
+ "cfg_aliases",
+ "libc",
+ "once_cell",
+ "socket2 0.6.1",
+ "tracing",
+ "windows-sys 0.60.2",
+]
+
[[package]]
name = "quote"
version = "1.0.41"
@@ -1648,14 +2594,35 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
+]
+
[[package]]
name = "rand"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
- "rand_chacha",
- "rand_core",
+ "rand_chacha 0.9.0",
+ "rand_core 0.9.3",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.6.4",
]
[[package]]
@@ -1665,7 +2632,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
- "rand_core",
+ "rand_core 0.9.3",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.16",
]
[[package]]
@@ -1677,6 +2653,76 @@ dependencies = [
"getrandom 0.3.4",
]
+[[package]]
+name = "rav1e"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b"
+dependencies = [
+ "aligned-vec",
+ "arbitrary",
+ "arg_enum_proc_macro",
+ "arrayvec",
+ "av-scenechange",
+ "av1-grain",
+ "bitstream-io",
+ "built",
+ "cfg-if 1.0.4",
+ "interpolate_name",
+ "itertools",
+ "libc",
+ "libfuzzer-sys",
+ "log",
+ "maybe-rayon",
+ "new_debug_unreachable",
+ "noop_proc_macro",
+ "num-derive",
+ "num-traits",
+ "paste",
+ "profiling",
+ "rand 0.9.2",
+ "rand_chacha 0.9.0",
+ "simd_helpers",
+ "thiserror 2.0.17",
+ "v_frame",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "ravif"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef69c1990ceef18a116855938e74793a5f7496ee907562bd0857b6ac734ab285"
+dependencies = [
+ "avif-serialize",
+ "imgref",
+ "loop9",
+ "quick-error",
+ "rav1e",
+ "rayon",
+ "rgb",
+]
+
+[[package]]
+name = "rayon"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
+dependencies = [
+ "crossbeam-deque",
+ "crossbeam-utils",
+]
+
[[package]]
name = "redox_syscall"
version = "0.5.18"
@@ -1686,6 +2732,37 @@ dependencies = [
"bitflags 2.10.0",
]
+[[package]]
+name = "redox_users"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
+dependencies = [
+ "getrandom 0.2.16",
+ "libredox",
+ "thiserror 2.0.17",
+]
+
+[[package]]
+name = "ref-cast"
+version = "1.0.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d"
+dependencies = [
+ "ref-cast-impl",
+]
+
+[[package]]
+name = "ref-cast-impl"
+version = "1.0.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.108",
+]
+
[[package]]
name = "regex"
version = "1.12.2"
@@ -1717,37 +2794,35 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "reqwest"
-version = "0.12.24"
+version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f"
+checksum = "04e9018c9d814e5f30cc16a0f03271aeab3571e609612d9fe78c1aa8d11c2f62"
dependencies = [
"base64",
"bytes",
"encoding_rs",
- "futures-channel",
"futures-core",
- "futures-util",
- "h2",
- "http",
- "http-body",
+ "h2 0.4.12",
+ "http 1.3.1",
+ "http-body 1.0.1",
"http-body-util",
- "hyper",
+ "hyper 1.8.1",
"hyper-rustls",
- "hyper-tls",
"hyper-util",
"js-sys",
"log",
"mime",
- "native-tls",
"percent-encoding",
"pin-project-lite",
+ "quinn",
+ "rustls",
"rustls-pki-types",
+ "rustls-platform-verifier",
"serde",
"serde_json",
- "serde_urlencoded",
"sync_wrapper",
"tokio",
- "tokio-native-tls",
+ "tokio-rustls",
"tower",
"tower-http",
"tower-service",
@@ -1757,6 +2832,12 @@ dependencies = [
"web-sys",
]
+[[package]]
+name = "rgb"
+version = "0.8.52"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce"
+
[[package]]
name = "ring"
version = "0.17.14"
@@ -1776,17 +2857,109 @@ name = "robonix-core"
version = "0.1.0"
dependencies = [
"ansi_term",
+ "anyhow",
+ "base64",
+ "byteorder",
"chrono",
+ "dirs",
"env_logger",
"futures",
"futures-util",
+ "image",
+ "libc",
"log",
+ "regex",
"reqwest",
+ "rocket",
"ros2-client",
"serde",
"serde_json",
+ "serde_yaml",
"smol",
"tokio",
+ "tokio-stream",
+]
+
+[[package]]
+name = "rocket"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a516907296a31df7dc04310e7043b61d71954d703b603cc6867a026d7e72d73f"
+dependencies = [
+ "async-stream",
+ "async-trait",
+ "atomic 0.5.3",
+ "binascii",
+ "bytes",
+ "either",
+ "figment",
+ "futures",
+ "indexmap",
+ "log",
+ "memchr",
+ "multer",
+ "num_cpus",
+ "parking_lot",
+ "pin-project-lite",
+ "rand 0.8.5",
+ "ref-cast",
+ "rocket_codegen",
+ "rocket_http",
+ "serde",
+ "serde_json",
+ "state",
+ "tempfile",
+ "time",
+ "tokio",
+ "tokio-stream",
+ "tokio-util",
+ "ubyte",
+ "version_check",
+ "yansi",
+]
+
+[[package]]
+name = "rocket_codegen"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "575d32d7ec1a9770108c879fc7c47815a80073f96ca07ff9525a94fcede1dd46"
+dependencies = [
+ "devise",
+ "glob",
+ "indexmap",
+ "proc-macro2",
+ "quote",
+ "rocket_http",
+ "syn 2.0.108",
+ "unicode-xid",
+ "version_check",
+]
+
+[[package]]
+name = "rocket_http"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e274915a20ee3065f611c044bd63c40757396b6dbc057d6046aec27f14f882b9"
+dependencies = [
+ "cookie",
+ "either",
+ "futures",
+ "http 0.2.12",
+ "hyper 0.14.32",
+ "indexmap",
+ "log",
+ "memchr",
+ "pear",
+ "percent-encoding",
+ "pin-project-lite",
+ "ref-cast",
+ "serde",
+ "smallvec",
+ "stable-pattern",
+ "state",
+ "time",
+ "tokio",
+ "uncased",
]
[[package]]
@@ -1818,6 +2991,12 @@ dependencies = [
"widestring",
]
+[[package]]
+name = "rustc-hash"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
+
[[package]]
name = "rustdds"
version = "0.11.6"
@@ -1845,7 +3024,7 @@ dependencies = [
"paste",
"pnet",
"pnet_sys",
- "rand",
+ "rand 0.9.2",
"serde",
"serde_repr",
"socket2 0.5.10",
@@ -1874,6 +3053,7 @@ version = "0.23.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f"
dependencies = [
+ "aws-lc-rs",
"once_cell",
"rustls-pki-types",
"rustls-webpki",
@@ -1881,21 +3061,62 @@ dependencies = [
"zeroize",
]
+[[package]]
+name = "rustls-native-certs"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63"
+dependencies = [
+ "openssl-probe",
+ "rustls-pki-types",
+ "schannel",
+ "security-framework",
+]
+
[[package]]
name = "rustls-pki-types"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a"
dependencies = [
+ "web-time",
"zeroize",
]
+[[package]]
+name = "rustls-platform-verifier"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
+dependencies = [
+ "core-foundation 0.10.1",
+ "core-foundation-sys",
+ "jni",
+ "log",
+ "once_cell",
+ "rustls",
+ "rustls-native-certs",
+ "rustls-platform-verifier-android",
+ "rustls-webpki",
+ "security-framework",
+ "security-framework-sys",
+ "webpki-root-certs",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustls-platform-verifier-android"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
+
[[package]]
name = "rustls-webpki"
version = "0.103.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52"
dependencies = [
+ "aws-lc-rs",
"ring",
"rustls-pki-types",
"untrusted",
@@ -1913,6 +3134,15 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
[[package]]
name = "schannel"
version = "0.1.28"
@@ -1922,6 +3152,12 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "scoped-tls"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
+
[[package]]
name = "scopeguard"
version = "1.2.0"
@@ -1930,12 +3166,12 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "security-framework"
-version = "2.11.1"
+version = "3.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
+checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef"
dependencies = [
"bitflags 2.10.0",
- "core-foundation",
+ "core-foundation 0.10.1",
"core-foundation-sys",
"libc",
"security-framework-sys",
@@ -2006,30 +3242,64 @@ dependencies = [
]
[[package]]
-name = "serde_urlencoded"
-version = "0.7.1"
+name = "serde_spanned"
+version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
dependencies = [
- "form_urlencoded",
+ "serde",
+]
+
+[[package]]
+name = "serde_yaml"
+version = "0.9.34+deprecated"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
+dependencies = [
+ "indexmap",
"itoa",
"ryu",
"serde",
+ "unsafe-libyaml",
+]
+
+[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
[[package]]
-name = "signal-hook-registry"
-version = "1.4.6"
+name = "simd_helpers"
+version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b"
+checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6"
dependencies = [
- "libc",
+ "quote",
]
[[package]]
@@ -2115,12 +3385,36 @@ dependencies = [
"syn 2.0.108",
]
+[[package]]
+name = "spin"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+
+[[package]]
+name = "stable-pattern"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4564168c00635f88eaed410d5efa8131afa8d8699a612c80c455a0ba05c21045"
+dependencies = [
+ "memchr",
+]
+
[[package]]
name = "stable_deref_trait"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+[[package]]
+name = "state"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b8c4a4445d81357df8b1a650d0d0d6fbbbfe99d064aa5e02f3e4022061476d8"
+dependencies = [
+ "loom",
+]
+
[[package]]
name = "static_assertions"
version = "1.1.0"
@@ -2188,7 +3482,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
dependencies = [
"bitflags 1.3.2",
- "core-foundation",
+ "core-foundation 0.9.4",
"system-configuration-sys",
]
@@ -2255,6 +3549,60 @@ dependencies = [
"syn 2.0.108",
]
+[[package]]
+name = "thread_local"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
+dependencies = [
+ "cfg-if 1.0.4",
+]
+
+[[package]]
+name = "tiff"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f"
+dependencies = [
+ "fax",
+ "flate2",
+ "half",
+ "quick-error",
+ "weezl",
+ "zune-jpeg 0.4.21",
+]
+
+[[package]]
+name = "time"
+version = "0.3.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d"
+dependencies = [
+ "deranged",
+ "itoa",
+ "num-conv",
+ "powerfmt",
+ "serde",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
+
+[[package]]
+name = "time-macros"
+version = "0.2.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
[[package]]
name = "tinystr"
version = "0.8.2"
@@ -2265,11 +3613,26 @@ dependencies = [
"zerovec",
]
+[[package]]
+name = "tinyvec"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
[[package]]
name = "tokio"
-version = "1.48.0"
+version = "1.49.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
+checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
dependencies = [
"bytes",
"libc",
@@ -2294,22 +3657,23 @@ dependencies = [
]
[[package]]
-name = "tokio-native-tls"
-version = "0.3.1"
+name = "tokio-rustls"
+version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
+checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
dependencies = [
- "native-tls",
+ "rustls",
"tokio",
]
[[package]]
-name = "tokio-rustls"
-version = "0.26.4"
+name = "tokio-stream"
+version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
+checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
dependencies = [
- "rustls",
+ "futures-core",
+ "pin-project-lite",
"tokio",
]
@@ -2326,6 +3690,47 @@ dependencies = [
"tokio",
]
+[[package]]
+name = "toml"
+version = "0.8.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
+dependencies = [
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.22.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
+dependencies = [
+ "indexmap",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_write",
+ "winnow",
+]
+
+[[package]]
+name = "toml_write"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
+
[[package]]
name = "tower"
version = "0.5.2"
@@ -2343,15 +3748,15 @@ dependencies = [
[[package]]
name = "tower-http"
-version = "0.6.6"
+version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
+checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
dependencies = [
"bitflags 2.10.0",
"bytes",
"futures-util",
- "http",
- "http-body",
+ "http 1.3.1",
+ "http-body 1.0.1",
"iri-string",
"pin-project-lite",
"tower",
@@ -2400,6 +3805,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
dependencies = [
"once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
+dependencies = [
+ "matchers",
+ "nu-ansi-term",
+ "once_cell",
+ "regex-automata",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
]
[[package]]
@@ -2408,12 +3843,43 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+[[package]]
+name = "ubyte"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f720def6ce1ee2fc44d40ac9ed6d3a59c361c80a75a7aa8e75bb9baed31cf2ea"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "uncased"
+version = "0.9.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697"
+dependencies = [
+ "serde",
+ "version_check",
+]
+
[[package]]
name = "unicode-ident"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06"
+[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[package]]
+name = "unsafe-libyaml"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
+
[[package]]
name = "untrusted"
version = "0.9.0"
@@ -2452,7 +3918,7 @@ checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
dependencies = [
"getrandom 0.3.4",
"js-sys",
- "rand",
+ "rand 0.9.2",
"serde",
"uuid-macro-internal",
"wasm-bindgen",
@@ -2470,10 +3936,37 @@ dependencies = [
]
[[package]]
-name = "vcpkg"
-version = "0.2.15"
+name = "v_frame"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2"
+dependencies = [
+ "aligned-vec",
+ "num-traits",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "valuable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
[[package]]
name = "want"
@@ -2567,6 +4060,31 @@ dependencies = [
"wasm-bindgen",
]
+[[package]]
+name = "web-time"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webpki-root-certs"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36a29fc0408b113f68cf32637857ab740edfafdf460c326cd2afaa2d84cc05dc"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "weezl"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
+
[[package]]
name = "widestring"
version = "1.2.1"
@@ -2601,12 +4119,30 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+[[package]]
+name = "winapi-util"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+[[package]]
+name = "windows"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
[[package]]
name = "windows-core"
version = "0.62.2"
@@ -2677,6 +4213,15 @@ dependencies = [
"windows-link",
]
+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets 0.42.2",
+]
+
[[package]]
name = "windows-sys"
version = "0.48.0"
@@ -2722,6 +4267,21 @@ dependencies = [
"windows-link",
]
+[[package]]
+name = "windows-targets"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
+dependencies = [
+ "windows_aarch64_gnullvm 0.42.2",
+ "windows_aarch64_msvc 0.42.2",
+ "windows_i686_gnu 0.42.2",
+ "windows_i686_msvc 0.42.2",
+ "windows_x86_64_gnu 0.42.2",
+ "windows_x86_64_gnullvm 0.42.2",
+ "windows_x86_64_msvc 0.42.2",
+]
+
[[package]]
name = "windows-targets"
version = "0.48.5"
@@ -2770,6 +4330,12 @@ dependencies = [
"windows_x86_64_msvc 0.53.1",
]
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
+
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
@@ -2788,6 +4354,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
@@ -2806,6 +4378,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
@@ -2836,6 +4414,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
@@ -2854,6 +4438,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
@@ -2872,6 +4462,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
@@ -2890,6 +4486,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
@@ -2908,6 +4510,15 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
+[[package]]
+name = "winnow"
+version = "0.7.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
+dependencies = [
+ "memchr",
+]
+
[[package]]
name = "wit-bindgen"
version = "0.46.0"
@@ -2930,6 +4541,21 @@ dependencies = [
"winapi-build",
]
+[[package]]
+name = "y4m"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448"
+
+[[package]]
+name = "yansi"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
+dependencies = [
+ "is-terminal",
+]
+
[[package]]
name = "yoke"
version = "0.8.1"
@@ -3032,3 +4658,42 @@ dependencies = [
"quote",
"syn 2.0.108",
]
+
+[[package]]
+name = "zune-core"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
+
+[[package]]
+name = "zune-core"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "111f7d9820f05fd715df3144e254d6fc02ee4088b0644c0ffd0efc9e6d9d2773"
+
+[[package]]
+name = "zune-inflate"
+version = "0.2.54"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02"
+dependencies = [
+ "simd-adler32",
+]
+
+[[package]]
+name = "zune-jpeg"
+version = "0.4.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713"
+dependencies = [
+ "zune-core 0.4.12",
+]
+
+[[package]]
+name = "zune-jpeg"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e35aee689668bf9bd6f6f3a6c60bb29ba1244b3b43adfd50edd554a371da37d5"
+dependencies = [
+ "zune-core 0.5.0",
+]
diff --git a/rust/robonix-core/Cargo.toml b/rust/robonix-core/Cargo.toml
index c0cdc5c..aed269a 100644
--- a/rust/robonix-core/Cargo.toml
+++ b/rust/robonix-core/Cargo.toml
@@ -11,8 +11,18 @@ futures = "0.3"
futures-util = "0.3"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
+serde_yaml = "0.9"
+dirs = "6.0"
+anyhow = "1.0"
smol = "2.0.2"
-tokio = { version = "1.0", features = ["full"] }
+tokio = { version = "1.49", features = ["full"] }
ansi_term = "0.12"
chrono = "0.4"
-reqwest = { version = "0.12.24", features = ["json", "blocking"] }
+reqwest = { version = "0.13.1", features = ["json"] }
+rocket = { version = "0.5", features = ["json"] }
+tokio-stream = "0.1"
+regex = "1.10"
+image = "0.25"
+byteorder = "1.5"
+libc = "0.2"
+base64 = "0.22"
diff --git a/rust/robonix-core/src/action/mod.rs b/rust/robonix-core/src/action/mod.rs
new file mode 100644
index 0000000..e0bacdc
--- /dev/null
+++ b/rust/robonix-core/src/action/mod.rs
@@ -0,0 +1,2 @@
+pub mod skill_library;
+pub mod specs;
diff --git a/rust/robonix-core/src/skill_library/mod.rs b/rust/robonix-core/src/action/skill_library/mod.rs
similarity index 100%
rename from rust/robonix-core/src/skill_library/mod.rs
rename to rust/robonix-core/src/action/skill_library/mod.rs
diff --git a/rust/robonix-core/src/skill_library/skill.rs b/rust/robonix-core/src/action/skill_library/skill.rs
similarity index 76%
rename from rust/robonix-core/src/skill_library/skill.rs
rename to rust/robonix-core/src/action/skill_library/skill.rs
index 44d809e..040e994 100644
--- a/rust/robonix-core/src/skill_library/skill.rs
+++ b/rust/robonix-core/src/action/skill_library/skill.rs
@@ -25,14 +25,38 @@ struct SkillEntry {
r#type: String, // Skill type: "basic" | "rtdl"
start_topic: String,
status_topic: String,
- entry: String, // Basic skill entry (if type="basic")
- skill_dir: String, // Skill directory path (if type="rtdl")
- main_rtdl: String, // Main RTDL file name (if type="rtdl")
- start_args: serde_json::Value,
- status: serde_json::Value,
- metadata: serde_json::Value,
+ entry: String, // Basic skill entry (if type="basic")
+ skill_dir: String, // Skill directory path (if type="rtdl")
+ main_rtdl: String, // Main RTDL file name (if type="rtdl")
+ start_args: serde_json::Value, // JSON: structured data for internal use
+ status: serde_json::Value, // JSON: structured data for internal use
+ metadata: serde_json::Value, // JSON: structured data for internal use
provider: String,
version: String,
+ node_id: String,
+}
+
+impl SkillEntry {
+ /// Convert to ROS2 message format (SkillInstance)
+ fn to_skill_instance(&self) -> SkillInstance {
+ SkillInstance {
+ skill_id: self.skill_id.clone(),
+ name: self.name.clone(),
+ provider: self.provider.clone(),
+ version: self.version.clone(),
+ r#type: self.r#type.clone(),
+ start_topic: self.start_topic.clone(),
+ status_topic: self.status_topic.clone(),
+ entry: self.entry.clone(),
+ skill_dir: self.skill_dir.clone(),
+ main_rtdl: self.main_rtdl.clone(),
+ start_args: serde_json::to_string(&self.start_args)
+ .unwrap_or_else(|_| "{}".to_string()),
+ status: serde_json::to_string(&self.status).unwrap_or_else(|_| "{}".to_string()),
+ metadata: serde_json::to_string(&self.metadata).unwrap_or_else(|_| "{}".to_string()),
+ node_id: self.node_id.clone(),
+ }
+ }
}
impl SkillRegistry {
@@ -46,7 +70,7 @@ impl SkillRegistry {
/// Register a skill
/// Note: Skills do not have specifications - they are user-defined and flexible
pub async fn register(&self, req: RegisterSkillRequest) -> RegisterSkillResponse {
- // Parse JSON strings
+ // Parse and validate JSON format
let start_args: serde_json::Value = match serde_json::from_str(&req.start_args) {
Ok(v) => v,
Err(e) => {
@@ -121,20 +145,18 @@ impl SkillRegistry {
};
}
- // Generate unique skill_id
+ // Generate unique skill_id (similar to primitive, no automatic prefix)
let counter = self
.skill_id_counter
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
- let skill_id = format!("skl_{}_{}", req.name.replace("::", "_"), counter);
-
- // Basic validation: skill name should start with 'skl::'
- if !req.name.starts_with("skl::") {
- warn!(
- "skill name should start with 'skl::': skill_name={}",
- req.name
- );
- }
+ // Use name directly, replace special characters for ID
+ let skill_id = format!(
+ "{}_{}",
+ req.name.replace("::", "_").replace("-", "_"),
+ counter
+ );
+ // Store with structured JSON internally
let entry = SkillEntry {
skill_id: skill_id.clone(),
name: req.name.clone(),
@@ -149,6 +171,7 @@ impl SkillRegistry {
metadata,
provider: req.provider.clone(),
version: req.version.clone(),
+ node_id: req.node_id.clone(),
};
let mut skills = self.skills.write().await;
@@ -197,6 +220,7 @@ impl SkillRegistry {
filter_obj.remove("type");
}
+ // Use structured metadata directly (no need to parse)
if !metadata_filter.is_null()
&& !self.matches_filter(&entry.metadata, &metadata_filter)
{
@@ -204,20 +228,8 @@ impl SkillRegistry {
}
}
- instances.push(SkillInstance {
- skill_id: entry.skill_id.clone(),
- provider: entry.provider.clone(),
- version: entry.version.clone(),
- r#type: entry.r#type.clone(),
- start_topic: entry.start_topic.clone(),
- status_topic: entry.status_topic.clone(),
- entry: entry.entry.clone(),
- skill_dir: entry.skill_dir.clone(),
- main_rtdl: entry.main_rtdl.clone(),
- start_args: entry.start_args.clone(),
- status: entry.status.clone(),
- metadata: entry.metadata.clone(),
- });
+ // Convert to ROS2 message format (serialize JSON fields to strings)
+ instances.push(entry.to_skill_instance());
}
QuerySkillResponse { instances }
@@ -226,24 +238,7 @@ impl SkillRegistry {
/// Get skill by ID (returns SkillInstance)
pub async fn get_skill_by_id(&self, skill_id: &str) -> Option {
let skills = self.skills.read().await;
- if let Some(entry) = skills.get(skill_id) {
- Some(SkillInstance {
- skill_id: entry.skill_id.clone(),
- provider: entry.provider.clone(),
- version: entry.version.clone(),
- r#type: entry.r#type.clone(),
- start_topic: entry.start_topic.clone(),
- status_topic: entry.status_topic.clone(),
- entry: entry.entry.clone(),
- skill_dir: entry.skill_dir.clone(),
- main_rtdl: entry.main_rtdl.clone(),
- start_args: entry.start_args.clone(),
- status: entry.status.clone(),
- metadata: entry.metadata.clone(),
- })
- } else {
- None
- }
+ skills.get(skill_id).map(|entry| entry.to_skill_instance())
}
/// Get all skill names
@@ -256,6 +251,15 @@ impl SkillRegistry {
names.into_iter().collect()
}
+ /// Get all registered skills (for web UI)
+ pub async fn get_all_skills(&self) -> Vec<(String, SkillInstance)> {
+ let skills = self.skills.read().await;
+ skills
+ .iter()
+ .map(|(skill_id, entry)| (skill_id.clone(), entry.to_skill_instance()))
+ .collect()
+ }
+
/// Check if metadata matches filter
fn matches_filter(&self, metadata: &serde_json::Value, filter: &serde_json::Value) -> bool {
if let (Some(meta_obj), Some(filter_obj)) = (metadata.as_object(), filter.as_object()) {
diff --git a/rust/robonix-core/src/action/specs.rs b/rust/robonix-core/src/action/specs.rs
new file mode 100644
index 0000000..672d786
--- /dev/null
+++ b/rust/robonix-core/src/action/specs.rs
@@ -0,0 +1,34 @@
+use crate::spec::{PrimitiveSpec, ServiceSpec};
+use std::collections::HashMap;
+
+pub fn load_primitives(primitives: &mut HashMap) {
+ PRM!(primitives, "prm::arm.move.ee", "Move end effector to target pose",
+ { "pose": "geometry_msgs/msg/PoseStamped" },
+ { "status": "std_msgs/msg/Bool" });
+
+ PRM!(primitives, "prm::gripper.close", "Close gripper",
+ {}, // No input parameters
+ { "status": "std_msgs/msg/Bool" });
+
+ PRM!(primitives, "prm::base.move", "Move mobile base with velocity command",
+ { "cmd_vel": "geometry_msgs/msg/Twist" },
+ { "odom": "nav_msgs/msg/Odometry" });
+
+ PRM!(primitives, "prm::base.navigate", "Navigate to target pose in map frame",
+ { "goal": "geometry_msgs/msg/PoseStamped" },
+ {});
+
+ PRM!(primitives, "prm::speech.tts", "Text-to-speech synthesis and playback",
+ { "text": "std_msgs/msg/String" },
+ { "status": "std_msgs/msg/Bool" });
+}
+
+pub fn load_services(services: &mut HashMap) {
+ SRV!(
+ // TODO
+ services,
+ "srv::control",
+ "Body control service providing motion control and safety management",
+ "robonix_sdk/srv/service/control/Control"
+ );
+}
diff --git a/rust/robonix-core/src/agent/agent.rs b/rust/robonix-core/src/agent/agent.rs
new file mode 100644
index 0000000..bab5c50
--- /dev/null
+++ b/rust/robonix-core/src/agent/agent.rs
@@ -0,0 +1,189 @@
+// SPDX-License-Identifier: MulanPSL-2.0
+// Agent Module
+//
+// Main agent implementation for natural language interaction
+
+use crate::agent::functions::FunctionRegistry;
+use crate::agent::llm::{AgentConfig, ChatMessage, LLMClient};
+use crate::core::RobonixCore;
+use crate::perception::image_monitor::ImageMonitor;
+use anyhow::{Context, Result};
+use serde::{Deserialize, Serialize};
+use serde_json::{Value, json};
+use std::sync::Arc;
+
+pub struct Agent {
+ llm_client: LLMClient,
+ function_registry: FunctionRegistry,
+ conversation_history: Vec,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct AgentRequest {
+ pub message: String,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct AgentResponse {
+ pub message: String,
+ pub function_calls: Vec,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct FunctionCallInfo {
+ pub name: String,
+ pub arguments: Value,
+ pub result: Value,
+}
+
+impl Agent {
+ fn create_system_message() -> ChatMessage {
+ ChatMessage {
+ role: "system".to_string(),
+ content: concat!(
+ "You are a helpful and friendly assistant for the Robonix robot system. ",
+ "Keep responses concise: answer directly and avoid long paragraphs or unnecessary elaboration unless the user explicitly asks for detail.\n\n",
+ "You can help users in two ways:\n\n",
+ "1. Robonix system operations: Help users query the semantic map, submit tasks, ",
+ "query system capabilities, describe what the robot sees (using describe_robot_vision when the user asks about the robot's view or current scene), and perform robot operations using function calls when appropriate.\n\n",
+ "2. General conversation: You can also engage in general conversations, answer questions, ",
+ "provide introductions, explain concepts, and have friendly chats with users. ",
+ "Don't limit yourself to only robot-related topics - be helpful and conversational ",
+ "about any topic the user asks about.\n\n",
+ "Always provide natural language responses and use function calls only when the user ",
+ "needs to interact with the Robonix system. For general questions, conversations, ",
+ "or introductions, respond naturally without function calls.\n\n",
+ "IMPORTANT FORMATTING RULES:\n",
+ "- Return only plain text without any markdown formatting (no **, ##, ```, etc.)\n",
+ "- Write in complete, natural sentences and paragraphs only\n",
+ "- NEVER use bullet points, numbered lists, or any list format (no \"1.\", \"2.\", \"-\", etc.)\n",
+ "- NEVER use colons followed by line breaks or lists (no \"可能意味着:\\n\" or similar patterns)\n",
+ "- When presenting multiple points, connect them with words like \"and\", \"also\", \"additionally\", ",
+ "or write separate sentences\n",
+ "- Ensure exactly one space between words and sentences - no extra spaces anywhere\n",
+ "- Write as if you're having a natural conversation, not formatting a document\n",
+ "- Example BAD (Chinese): \"可能意味着:\\n机器人当前所在的区域比较空旷\\n或者语义地图还没有加载\"\n",
+ "- Example GOOD (Chinese): \"这可能意味着机器人当前所在的区域比较空旷,或者语义地图还没有加载。\"\n",
+ "- Example BAD (English): \"This may mean:\\nThe robot is in an empty area\\nor the semantic map hasn't loaded\"\n",
+ "- Example GOOD (English): \"This may mean the robot is in an empty area, or the semantic map hasn't loaded yet.\"\n",
+ "- Example BAD (Chinese): \"活跃任务:0个\\n注册的基础操作:0个\"\n",
+ "- Example GOOD (Chinese): \"当前系统中有0个活跃任务和0个注册的基础操作。\"\n",
+ "- Example BAD (English): \"Active tasks: 0\\nRegistered primitives: 0\"\n",
+ "- Example GOOD (English): \"Currently there are 0 active tasks and 0 registered primitives in the system.\""
+ ).to_string(),
+ }
+ }
+
+ pub fn new(
+ core: Arc,
+ config: AgentConfig,
+ image_monitor: Arc,
+ ) -> Self {
+ let llm_client = LLMClient::new(config.clone());
+ let function_registry = FunctionRegistry::new(core, image_monitor, config);
+
+ // Initialize with system message
+ let mut conversation_history = Vec::new();
+ conversation_history.push(Self::create_system_message());
+
+ Self {
+ llm_client,
+ function_registry,
+ conversation_history,
+ }
+ }
+
+ pub async fn chat(&mut self, request: AgentRequest) -> Result {
+ // Add user message to history
+ self.conversation_history.push(ChatMessage {
+ role: "user".to_string(),
+ content: request.message,
+ });
+
+ // Get function schemas
+ let functions = self.function_registry.get_function_schemas();
+
+ // Call LLM
+ let llm_response = self
+ .llm_client
+ .chat(self.conversation_history.clone(), functions)
+ .await
+ .context("Failed to get LLM response")?;
+
+ // Execute function calls and collect results
+ let mut function_calls_info = Vec::new();
+ for function_call in &llm_response.function_calls {
+ let result = match self
+ .function_registry
+ .call_function(&function_call.name, function_call.arguments.clone())
+ .await
+ {
+ Ok(result) => result,
+ Err(e) => {
+ json!({
+ "error": format!("Function execution failed: {}", e)
+ })
+ }
+ };
+
+ function_calls_info.push(FunctionCallInfo {
+ name: function_call.name.clone(),
+ arguments: function_call.arguments.clone(),
+ result,
+ });
+ }
+
+ // If there were function calls, add them to conversation and get final response
+ let final_message = if !llm_response.function_calls.is_empty() {
+ // Add assistant message with function calls
+ let mut assistant_message = format!("{}\n\n", llm_response.message);
+ for function_call_info in &function_calls_info {
+ assistant_message.push_str(&format!(
+ "Function {} returned: {}\n",
+ function_call_info.name,
+ serde_json::to_string_pretty(&function_call_info.result).unwrap_or_default()
+ ));
+ }
+
+ // Add to conversation
+ self.conversation_history.push(ChatMessage {
+ role: "assistant".to_string(),
+ content: assistant_message.clone(),
+ });
+
+ // Get final response from LLM with function results
+ let final_functions = self.function_registry.get_function_schemas();
+ let final_response = self
+ .llm_client
+ .chat(self.conversation_history.clone(), final_functions)
+ .await
+ .context("Failed to get final LLM response")?;
+
+ // Update conversation history
+ self.conversation_history.push(ChatMessage {
+ role: "assistant".to_string(),
+ content: final_response.message.clone(),
+ });
+
+ final_response.message
+ } else {
+ // No function calls, just use the message
+ self.conversation_history.push(ChatMessage {
+ role: "assistant".to_string(),
+ content: llm_response.message.clone(),
+ });
+ llm_response.message
+ };
+
+ Ok(AgentResponse {
+ message: final_message,
+ function_calls: function_calls_info,
+ })
+ }
+
+ pub fn clear_history(&mut self) {
+ self.conversation_history.clear();
+ self.conversation_history
+ .push(Self::create_system_message());
+ }
+}
diff --git a/rust/robonix-core/src/agent/functions.rs b/rust/robonix-core/src/agent/functions.rs
new file mode 100644
index 0000000..4b10b46
--- /dev/null
+++ b/rust/robonix-core/src/agent/functions.rs
@@ -0,0 +1,671 @@
+// SPDX-License-Identifier: MulanPSL-2.0
+// Function Registry Module
+//
+// Registry for system functions that can be called by the agent
+
+use crate::agent::llm::{AgentConfig, LLMClient};
+use crate::core::RobonixCore;
+use crate::perception::image_monitor::ImageMonitor;
+use anyhow::{Context, Result};
+use base64::Engine;
+use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
+use serde_json::{Value, json};
+use std::sync::Arc;
+
+pub struct FunctionRegistry {
+ core: Arc,
+ image_monitor: Arc,
+ agent_config: AgentConfig,
+}
+
+impl FunctionRegistry {
+ pub fn new(
+ core: Arc,
+ image_monitor: Arc,
+ agent_config: AgentConfig,
+ ) -> Self {
+ Self {
+ core,
+ image_monitor,
+ agent_config,
+ }
+ }
+
+ pub fn get_function_schemas(&self) -> Vec {
+ vec![
+ json!({
+ "type": "function",
+ "function": {
+ "name": "query_nearest_objects",
+ "description": "Query the nearest objects from the semantic map relative to the robot. Returns a list of objects sorted by distance.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "count": {
+ "type": "integer",
+ "description": "Number of nearest objects to return (default: 5)",
+ "default": 5
+ },
+ "types": {
+ "type": "array",
+ "items": {"type": "string"},
+ "description": "Filter objects by type (optional). Examples: ['table', 'chair', 'box']"
+ }
+ }
+ }
+ }
+ }),
+ json!({
+ "type": "function",
+ "function": {
+ "name": "infer_environment",
+ "description": "Infer the current environment/room type based on objects in the semantic map. Returns a description of the environment.",
+ "parameters": {
+ "type": "object",
+ "properties": {}
+ }
+ }
+ }),
+ json!({
+ "type": "function",
+ "function": {
+ "name": "submit_task",
+ "description": "Submit a task to the system. The task will be planned and executed by the task manager.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "description": {
+ "type": "string",
+ "description": "Natural language description of the task to execute"
+ }
+ },
+ "required": ["description"]
+ }
+ }
+ }),
+ json!({
+ "type": "function",
+ "function": {
+ "name": "query_skills",
+ "description": "Query all registered skills in the system. Returns a list of available skills with their metadata.",
+ "parameters": {
+ "type": "object",
+ "properties": {}
+ }
+ }
+ }),
+ json!({
+ "type": "function",
+ "function": {
+ "name": "query_services",
+ "description": "Query all registered services in the system. Returns a list of available services with their metadata.",
+ "parameters": {
+ "type": "object",
+ "properties": {}
+ }
+ }
+ }),
+ json!({
+ "type": "function",
+ "function": {
+ "name": "query_primitives",
+ "description": "Query all registered primitives in the system. Returns a list of available primitives with their metadata.",
+ "parameters": {
+ "type": "object",
+ "properties": {}
+ }
+ }
+ }),
+ json!({
+ "type": "function",
+ "function": {
+ "name": "get_system_status",
+ "description": "Get the current system status including active tasks, registered skills, services, and primitives.",
+ "parameters": {
+ "type": "object",
+ "properties": {}
+ }
+ }
+ }),
+ json!({
+ "type": "function",
+ "function": {
+ "name": "list_tasks",
+ "description": "Get a list of all submitted tasks. Returns task IDs, descriptions, states, and basic information.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "state_filter": {
+ "type": "string",
+ "description": "Optional filter by task state: 'active' (pending/planning/running/suspended), 'finished', 'failed', 'cancelled', or 'all' (default: 'all')",
+ "enum": ["all", "active", "finished", "failed", "cancelled"]
+ },
+ "limit": {
+ "type": "integer",
+ "description": "Maximum number of tasks to return (default: 50, max: 100)"
+ }
+ }
+ }
+ }
+ }),
+ json!({
+ "type": "function",
+ "function": {
+ "name": "get_task_details",
+ "description": "Get detailed information about a specific task, including RTDL understanding, execution status, results, and error messages.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "task_id": {
+ "type": "string",
+ "description": "The task ID to get details for"
+ }
+ },
+ "required": ["task_id"]
+ }
+ }
+ }),
+ json!({
+ "type": "function",
+ "function": {
+ "name": "describe_robot_vision",
+ "description": "Describe what the robot currently sees from its RGB cameras. Use when the user asks about the robot's view, e.g. 'tell me what the robot sees', 'describe the current scene', '告诉我机器人目前看到的画面', 'what does the robot see?'. Uses the latest RGB images from the image monitor and a vision-language model to answer.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "question": {
+ "type": "string",
+ "description": "The user's question about the robot's view. Default: 'Describe what you see in these images from the robot\'s cameras.'"
+ }
+ }
+ }
+ }
+ }),
+ ]
+ }
+
+ pub async fn call_function(&self, name: &str, arguments: Value) -> Result {
+ match name {
+ "query_nearest_objects" => self.query_nearest_objects(arguments).await,
+ "infer_environment" => self.infer_environment().await,
+ "submit_task" => self.submit_task(arguments).await,
+ "query_skills" => self.query_skills().await,
+ "query_services" => self.query_services().await,
+ "query_primitives" => self.query_primitives().await,
+ "get_system_status" => self.get_system_status().await,
+ "list_tasks" => self.list_tasks(arguments).await,
+ "get_task_details" => self.get_task_details(arguments).await,
+ "describe_robot_vision" => self.describe_robot_vision(arguments).await,
+ _ => anyhow::bail!("Unknown function: {}", name),
+ }
+ }
+
+ async fn query_nearest_objects(&self, arguments: Value) -> Result {
+ let count = arguments.get("count").and_then(|c| c.as_u64()).unwrap_or(5) as usize;
+
+ let types_filter: Vec = arguments
+ .get("types")
+ .and_then(|t| t.as_array())
+ .map(|arr| {
+ arr.iter()
+ .filter_map(|v| v.as_str().map(|s| s.to_string()))
+ .collect()
+ })
+ .unwrap_or_default();
+
+ // Get semantic map from cache
+ // Cache is directly an array of objects, not an object with "objects" field
+ let task_manager = self.core.get_task_manager();
+ let cache = task_manager.get_semantic_map_cache();
+ let cache_guard = cache.lock().await;
+ let objects = cache_guard.as_array().cloned().unwrap_or_default();
+ drop(cache_guard);
+
+ // Calculate distances (simplified: assume robot is at origin for now)
+ // In a real implementation, we'd get robot pose from TF tree
+ let mut objects_with_distance: Vec<(Value, Option)> = objects
+ .into_iter()
+ .filter_map(|obj| {
+ // Filter by type if specified
+ // Object has "label" field (not "type"), but we check both for compatibility
+ if !types_filter.is_empty() {
+ let obj_type = obj
+ .get("type")
+ .and_then(|t| t.as_str())
+ .or_else(|| obj.get("label").and_then(|l| l.as_str()))
+ .unwrap_or("");
+ // Case-insensitive comparison
+ let obj_type_lower = obj_type.to_lowercase();
+ if !types_filter
+ .iter()
+ .any(|filter| filter.to_lowercase() == obj_type_lower)
+ {
+ return None;
+ }
+ }
+
+ // Calculate distance from origin (robot position)
+ // Objects have frame_mapping with center coordinates
+ // Prefer "map" frame over "base_link" for more accurate distance calculation
+ let mut distance_opt: Option = None;
+ if let Some(frame_mappings) = obj.get("frame_mapping").and_then(|f| f.as_array()) {
+ // First pass: look for "map" frame (preferred)
+ for mapping in frame_mappings.iter() {
+ if let Some(frame_id) = mapping.get("frame_id").and_then(|f| f.as_str()) {
+ if frame_id == "map" {
+ if let Some(center) = mapping.get("center") {
+ let x = center.get("x").and_then(|x| x.as_f64()).unwrap_or(0.0);
+ let y = center.get("y").and_then(|y| y.as_f64()).unwrap_or(0.0);
+ let z = center.get("z").and_then(|z| z.as_f64()).unwrap_or(0.0);
+ let distance = (x * x + y * y + z * z).sqrt();
+ distance_opt = Some(distance);
+ break; // Found map frame, use it
+ }
+ }
+ }
+ }
+ // Second pass: if no map frame found, try base_link
+ if distance_opt.is_none() {
+ for mapping in frame_mappings.iter() {
+ if let Some(frame_id) = mapping.get("frame_id").and_then(|f| f.as_str())
+ {
+ if frame_id == "base_link" {
+ if let Some(center) = mapping.get("center") {
+ let x =
+ center.get("x").and_then(|x| x.as_f64()).unwrap_or(0.0);
+ let y =
+ center.get("y").and_then(|y| y.as_f64()).unwrap_or(0.0);
+ let z =
+ center.get("z").and_then(|z| z.as_f64()).unwrap_or(0.0);
+ let distance = (x * x + y * y + z * z).sqrt();
+ distance_opt = Some(distance);
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+ // Return object even if no distance available (distance will be None)
+ Some((obj.clone(), distance_opt))
+ })
+ .collect();
+
+ // Sort by distance: objects with distance first, then by distance value
+ // Objects without distance go to the end
+ objects_with_distance.sort_by(|a, b| match (a.1, b.1) {
+ (Some(da), Some(db)) => da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal),
+ (Some(_), None) => std::cmp::Ordering::Less,
+ (None, Some(_)) => std::cmp::Ordering::Greater,
+ (None, None) => std::cmp::Ordering::Equal,
+ });
+
+ // Take nearest N objects
+ let nearest: Vec = objects_with_distance
+ .into_iter()
+ .take(count)
+ .map(|(obj, distance_opt)| {
+ let mut result = obj.clone();
+ if let Some(distance) = distance_opt {
+ result
+ .as_object_mut()
+ .unwrap()
+ .insert("distance".to_string(), json!(distance));
+ } else {
+ // Mark objects without distance information
+ result
+ .as_object_mut()
+ .unwrap()
+ .insert("distance".to_string(), json!(null));
+ }
+ result
+ })
+ .collect();
+
+ Ok(json!({
+ "objects": nearest,
+ "count": nearest.len()
+ }))
+ }
+
+ async fn infer_environment(&self) -> Result {
+ // Get semantic map
+ // Cache is directly an array of objects, not an object with "objects" field
+ let task_manager = self.core.get_task_manager();
+ let cache = task_manager.get_semantic_map_cache();
+ let cache_guard = cache.lock().await;
+ let objects = cache_guard.as_array().cloned().unwrap_or_default();
+ drop(cache_guard);
+
+ // Simple heuristic-based environment inference
+ // Object has "label" field (not "type"), but we check both for compatibility
+ let mut object_types: Vec = objects
+ .iter()
+ .filter_map(|obj| {
+ obj.get("type")
+ .and_then(|t| t.as_str())
+ .or_else(|| obj.get("label").and_then(|l| l.as_str()))
+ .map(|s| s.to_string())
+ })
+ .collect();
+
+ object_types.sort();
+ object_types.dedup();
+
+ let environment = if object_types
+ .iter()
+ .any(|t| t.contains("table") || t.contains("desk"))
+ {
+ if object_types.iter().any(|t| t.contains("chair")) {
+ "office or workspace"
+ } else {
+ "workspace"
+ }
+ } else if object_types
+ .iter()
+ .any(|t| t.contains("box") || t.contains("container"))
+ {
+ "storage area"
+ } else if object_types.is_empty() {
+ "empty space"
+ } else {
+ "unknown environment"
+ };
+
+ Ok(json!({
+ "environment": environment,
+ "object_types": object_types,
+ "object_count": objects.len()
+ }))
+ }
+
+ async fn submit_task(&self, arguments: Value) -> Result {
+ let description = arguments
+ .get("description")
+ .and_then(|d| d.as_str())
+ .context("Missing task description")?;
+
+ let task_manager = self.core.get_task_manager();
+ let request = crate::task::api::SubmitTaskRequest {
+ description: description.to_string(),
+ params: "{}".to_string(),
+ };
+ let response = task_manager.submit_task(request).await;
+
+ Ok(json!({
+ "task_id": response.task_id,
+ "status": "submitted",
+ "description": description
+ }))
+ }
+
+ async fn query_skills(&self) -> Result {
+ let skill_library = self.core.get_skill_library();
+ let registry = skill_library.get_registry();
+ let skills = registry.get_all_skills().await;
+
+ let skills_json: Vec = skills
+ .into_iter()
+ .map(|(skill_id, instance)| {
+ json!({
+ "skill_id": skill_id,
+ "name": instance.name,
+ "provider": instance.provider,
+ "version": instance.version,
+ "type": instance.r#type,
+ })
+ })
+ .collect();
+
+ Ok(json!({
+ "skills": skills_json,
+ "count": skills_json.len()
+ }))
+ }
+
+ async fn query_services(&self) -> Result {
+ let service_registry = self.core.get_service_registry();
+ let services = service_registry.get_all_services().await;
+
+ let services_json: Vec = services
+ .into_iter()
+ .map(|(key, instance)| {
+ json!({
+ "key": key,
+ "provider": instance.provider,
+ "version": instance.version,
+ })
+ })
+ .collect();
+
+ Ok(json!({
+ "services": services_json,
+ "count": services_json.len()
+ }))
+ }
+
+ async fn query_primitives(&self) -> Result {
+ let primitive_registry = self.core.get_primitive_registry();
+ let primitives = primitive_registry.get_all_primitives().await;
+
+ let primitives_json: Vec = primitives
+ .into_iter()
+ .map(|(key, instance)| {
+ json!({
+ "key": key,
+ "name": key.split('$').next().unwrap_or(&key),
+ "provider": instance.provider,
+ "version": instance.version,
+ })
+ })
+ .collect();
+
+ Ok(json!({
+ "primitives": primitives_json,
+ "count": primitives_json.len()
+ }))
+ }
+
+ async fn get_system_status(&self) -> Result {
+ let task_manager = self.core.get_task_manager();
+ let skill_library = self.core.get_skill_library();
+ let service_registry = self.core.get_service_registry();
+ let primitive_registry = self.core.get_primitive_registry();
+
+ let task_store = task_manager.get_task_store();
+ let all_tasks = task_store.get_all_tasks().await;
+ let active_tasks = all_tasks
+ .iter()
+ .filter(|task| {
+ !matches!(
+ task.state,
+ crate::task::task::TaskState::Finished
+ | crate::task::task::TaskState::Failed
+ | crate::task::task::TaskState::Cancelled
+ )
+ })
+ .count();
+
+ let skills = skill_library.get_registry().get_all_skills().await;
+ let services = service_registry.get_all_services().await;
+ let primitives = primitive_registry.get_all_primitives().await;
+
+ Ok(json!({
+ "active_tasks": active_tasks,
+ "total_tasks": all_tasks.len(),
+ "registered_skills": skills.len(),
+ "registered_services": services.len(),
+ "registered_primitives": primitives.len(),
+ }))
+ }
+
+ async fn list_tasks(&self, arguments: Value) -> Result {
+ let task_manager = self.core.get_task_manager();
+ let task_store = task_manager.get_task_store();
+ let all_tasks = task_store.get_all_tasks().await;
+
+ // Parse filter
+ let state_filter = arguments
+ .get("state_filter")
+ .and_then(|s| s.as_str())
+ .unwrap_or("all");
+
+ let limit = arguments
+ .get("limit")
+ .and_then(|l| l.as_u64())
+ .map(|l| l.min(100) as usize)
+ .unwrap_or(50);
+
+ // Filter tasks by state
+ let filtered_tasks: Vec<_> = all_tasks
+ .into_iter()
+ .filter(|task| {
+ match state_filter {
+ "active" => {
+ matches!(
+ task.state,
+ crate::task::task::TaskState::Pending
+ | crate::task::task::TaskState::Planning
+ | crate::task::task::TaskState::Running
+ | crate::task::task::TaskState::Suspended
+ )
+ }
+ "finished" => matches!(task.state, crate::task::task::TaskState::Finished),
+ "failed" => matches!(task.state, crate::task::task::TaskState::Failed),
+ "cancelled" => matches!(task.state, crate::task::task::TaskState::Cancelled),
+ _ => true, // "all"
+ }
+ })
+ .take(limit)
+ .collect();
+
+ // Sort by created_at (newest first)
+ let mut sorted_tasks = filtered_tasks;
+ sorted_tasks.sort_by(|a, b| b.created_at.cmp(&a.created_at));
+
+ let tasks_json: Vec = sorted_tasks
+ .into_iter()
+ .map(|task| {
+ json!({
+ "task_id": task.task_id,
+ "description": task.description,
+ "state": format!("{:?}", task.state),
+ "priority": task.context.priority,
+ "created_at": task.created_at,
+ "updated_at": task.updated_at,
+ "has_rtdl": task.context.rtdl.is_some(),
+ "has_result": task.result.is_some(),
+ "has_error": task.error_message.is_some(),
+ })
+ })
+ .collect();
+
+ Ok(json!({
+ "tasks": tasks_json,
+ "count": tasks_json.len(),
+ "filter": state_filter
+ }))
+ }
+
+ async fn get_task_details(&self, arguments: Value) -> Result {
+ let task_id = arguments
+ .get("task_id")
+ .and_then(|id| id.as_str())
+ .context("Missing task_id parameter")?;
+
+ let task_manager = self.core.get_task_manager();
+ let task_store = task_manager.get_task_store();
+
+ if let Some(task) = task_store.get_task(task_id).await {
+ // Build comprehensive task details
+ let mut details = json!({
+ "task_id": task.task_id,
+ "description": task.description,
+ "params": task.params,
+ "state": format!("{:?}", task.state),
+ "priority": task.context.priority,
+ "created_at": task.created_at,
+ "updated_at": task.updated_at,
+ "retry_count": task.context.retry_count,
+ "rtdl_instruction_pointer": task.context.rtdl_instruction_pointer,
+ });
+
+ // RTDL information
+ if let Some(ref rtdl) = task.context.rtdl {
+ details["rtdl"] = json!(rtdl);
+ }
+ if let Some(ref rtdl_type) = task.context.rtdl_type {
+ details["rtdl_type"] = json!(rtdl_type);
+ }
+
+ // Execution status
+ details["execution_status"] = json!({
+ "state": format!("{:?}", task.state),
+ "current_instruction_index": task.context.rtdl_instruction_pointer,
+ "retry_count": task.context.retry_count,
+ });
+
+ if let Some(ref exception) = task.context.last_exception {
+ details["last_exception"] = json!(exception);
+ }
+
+ // Task results
+ if let Some(ref result) = task.result {
+ details["result"] = result.clone();
+ }
+
+ if let Some(ref error_msg) = task.error_message {
+ details["error_message"] = json!(error_msg);
+ }
+
+ // Object graph information
+ details["object_graph"] = task.context.object_graph.clone();
+ details["object_graph_updated_at"] = json!(task.context.object_graph_updated_at);
+ if let Some(arr) = task.context.object_graph.as_array() {
+ details["object_graph_count"] = json!(arr.len());
+ } else {
+ details["object_graph_count"] = json!(0);
+ }
+
+ Ok(details)
+ } else {
+ anyhow::bail!("Task not found: {}", task_id)
+ }
+ }
+
+ async fn describe_robot_vision(&self, arguments: Value) -> Result {
+ let question = arguments
+ .get("question")
+ .and_then(|q| q.as_str())
+ .unwrap_or("Describe what you see in these images from the robot's cameras.")
+ .to_string();
+
+ let paths = self.image_monitor.get_rgb_image_paths().await;
+ if paths.is_empty() {
+ return Ok(json!({
+ "answer": "No RGB camera images are currently available. The image monitor has not received any RGB image topics yet, or no topics with RGB encoding (rgb8/bgr8/rgba8/bgra8) are subscribed.",
+ "image_count": 0
+ }));
+ }
+
+ let mut image_base64_urls = Vec::with_capacity(paths.len());
+ for path in &paths {
+ let data = tokio::fs::read(path)
+ .await
+ .context("Failed to read image file")?;
+ let b64 = BASE64_STANDARD.encode(&data);
+ image_base64_urls.push(format!("data:image/jpeg;base64,{}", b64));
+ }
+
+ let client = LLMClient::new(self.agent_config.clone());
+ let answer = client
+ .chat_vision(image_base64_urls, &question)
+ .await
+ .context("VLM call failed")?;
+
+ Ok(json!({
+ "answer": answer,
+ "image_count": paths.len()
+ }))
+ }
+}
diff --git a/rust/robonix-core/src/agent/llm.rs b/rust/robonix-core/src/agent/llm.rs
new file mode 100644
index 0000000..e67adc6
--- /dev/null
+++ b/rust/robonix-core/src/agent/llm.rs
@@ -0,0 +1,274 @@
+// SPDX-License-Identifier: MulanPSL-2.0
+// LLM Client Module
+//
+// LLM API client for agent interactions
+
+use anyhow::{Context, Result};
+use serde::{Deserialize, Serialize};
+use serde_json::json;
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct AgentConfig {
+ pub llm_provider: String,
+ pub api_key: Option,
+ pub api_base: String,
+ pub model: String,
+ pub temperature: f64,
+}
+
+impl Default for AgentConfig {
+ fn default() -> Self {
+ Self {
+ llm_provider: "qwen".to_string(),
+ api_key: None,
+ // 华北2(北京)地域,与百炼/阿里云控制台一致;新加坡用 dashscope-intl.aliyuncs.com
+ api_base: "https://dashscope.aliyuncs.com/compatible-mode/v1".to_string(),
+ model: "qwen3-vl-plus".to_string(),
+ temperature: 0.7,
+ }
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ChatMessage {
+ pub role: String,
+ pub content: String,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct FunctionCall {
+ pub name: String,
+ pub arguments: serde_json::Value,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct LLMResponse {
+ pub message: String,
+ pub function_calls: Vec,
+}
+
+pub struct LLMClient {
+ config: AgentConfig,
+ client: reqwest::Client,
+}
+
+impl LLMClient {
+ pub fn new(config: AgentConfig) -> Self {
+ Self {
+ config,
+ client: reqwest::Client::new(),
+ }
+ }
+
+ pub async fn chat(
+ &self,
+ messages: Vec,
+ functions: Vec,
+ ) -> Result {
+ match self.config.llm_provider.as_str() {
+ "deepseek" => self.chat_openai_compat(messages, functions).await,
+ "qwen" => self.chat_openai_compat(messages, functions).await,
+ _ => anyhow::bail!("Unsupported LLM provider: {}", self.config.llm_provider),
+ }
+ }
+
+ /// OpenAI-compatible chat completions (used by DeepSeek and Qwen).
+ async fn chat_openai_compat(
+ &self,
+ messages: Vec,
+ functions: Vec,
+ ) -> Result {
+ let api_key = self
+ .config
+ .api_key
+ .as_ref()
+ .context("API key not configured")?;
+
+ // If api_base already ends with /v1 (e.g. DashScope .../compatible-mode/v1), use /chat/completions; else /v1/chat/completions (e.g. DeepSeek)
+ let base = self.config.api_base.trim_end_matches('/');
+ let path = if base.ends_with("/v1") {
+ "/chat/completions"
+ } else {
+ "/v1/chat/completions"
+ };
+ let url = format!("{}{}", base, path);
+
+ let mut request_body = json!({
+ "model": self.config.model,
+ "messages": messages,
+ "temperature": self.config.temperature,
+ "max_tokens": 1024,
+ });
+
+ if !functions.is_empty() {
+ request_body["tools"] = json!(functions);
+ request_body["tool_choice"] = json!("auto");
+ }
+
+ let response = self
+ .client
+ .post(&url)
+ .header("Authorization", format!("Bearer {}", api_key))
+ .header("Content-Type", "application/json")
+ .json(&request_body)
+ .send()
+ .await
+ .context("Failed to send request to LLM API")?;
+
+ if !response.status().is_success() {
+ let status = response.status();
+ let text = response.text().await.unwrap_or_default();
+ anyhow::bail!("LLM API error: {} - {}", status, text);
+ }
+
+ let response_json: serde_json::Value = response
+ .json()
+ .await
+ .context("Failed to parse LLM API response")?;
+
+ // Parse response
+ let choices = response_json
+ .get("choices")
+ .and_then(|c| c.as_array())
+ .context("Invalid response format: missing choices")?;
+
+ if choices.is_empty() {
+ anyhow::bail!("No choices in LLM response");
+ }
+
+ let choice = &choices[0];
+ let message = choice
+ .get("message")
+ .context("Invalid response format: missing message")?;
+
+ // Extract text content
+ let content = message
+ .get("content")
+ .and_then(|c| c.as_str())
+ .unwrap_or("")
+ .to_string();
+
+ // Extract function calls
+ let mut function_calls = Vec::new();
+ if let Some(tool_calls) = message.get("tool_calls") {
+ if let Some(tool_calls_array) = tool_calls.as_array() {
+ for tool_call in tool_calls_array {
+ if let Some(function) = tool_call.get("function") {
+ let name = function
+ .get("name")
+ .and_then(|n| n.as_str())
+ .context("Invalid tool call: missing name")?
+ .to_string();
+ let arguments_str = function
+ .get("arguments")
+ .and_then(|a| a.as_str())
+ .unwrap_or("{}");
+ let arguments: serde_json::Value = serde_json::from_str(arguments_str)
+ .context("Failed to parse function arguments")?;
+ function_calls.push(FunctionCall { name, arguments });
+ }
+ }
+ }
+ }
+
+ Ok(LLMResponse {
+ message: content,
+ function_calls,
+ })
+ }
+
+ /// Call Qwen3-VL (or compatible VLM) with images and a text question.
+ /// Each element of image_base64_urls should be "data:image/jpeg;base64,...".
+ pub async fn chat_vision(
+ &self,
+ image_base64_urls: Vec,
+ question: &str,
+ ) -> Result {
+ let api_key = self
+ .config
+ .api_key
+ .as_ref()
+ .context("API key not configured for VLM")?;
+
+ // If api_base already ends with /v1 (e.g. DashScope .../compatible-mode/v1), use /chat/completions; else /v1/chat/completions (e.g. DeepSeek)
+ let base = self.config.api_base.trim_end_matches('/');
+ let path = if base.ends_with("/v1") {
+ "/chat/completions"
+ } else {
+ "/v1/chat/completions"
+ };
+ let url = format!("{}{}", base, path);
+
+ // Build multimodal user content: image_url entries + text
+ let mut content: Vec = Vec::new();
+ for url in &image_base64_urls {
+ content.push(json!({
+ "type": "image_url",
+ "image_url": { "url": url }
+ }));
+ }
+ content.push(json!({
+ "type": "text",
+ "text": question
+ }));
+
+ let mut request_body = json!({
+ "model": "qwen3-vl-plus",
+ "messages": [
+ {
+ "role": "user",
+ "content": content
+ }
+ ],
+ "temperature": self.config.temperature,
+ "max_tokens": 2048,
+ });
+
+ // Qwen thinking (optional)
+ if self.config.llm_provider == "qwen" {
+ request_body["extra_body"] = json!({
+ "enable_thinking": false,
+ "thinking_budget": 8192
+ });
+ }
+
+ let response = self
+ .client
+ .post(&url)
+ .header("Authorization", format!("Bearer {}", api_key))
+ .header("Content-Type", "application/json")
+ .json(&request_body)
+ .send()
+ .await
+ .context("Failed to send request to VLM API")?;
+
+ if !response.status().is_success() {
+ let status = response.status();
+ let text = response.text().await.unwrap_or_default();
+ anyhow::bail!("VLM API error: {} - {}", status, text);
+ }
+
+ let response_json: serde_json::Value = response
+ .json()
+ .await
+ .context("Failed to parse VLM API response")?;
+
+ let choices = response_json
+ .get("choices")
+ .and_then(|c| c.as_array())
+ .context("Invalid response format: missing choices")?;
+
+ if choices.is_empty() {
+ anyhow::bail!("No choices in VLM response");
+ }
+
+ let content = choices[0]
+ .get("message")
+ .and_then(|m| m.get("content"))
+ .and_then(|c| c.as_str())
+ .unwrap_or("")
+ .to_string();
+
+ Ok(content)
+ }
+}
diff --git a/rust/robonix-core/src/agent/mod.rs b/rust/robonix-core/src/agent/mod.rs
new file mode 100644
index 0000000..5f4ef1e
--- /dev/null
+++ b/rust/robonix-core/src/agent/mod.rs
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: MulanPSL-2.0
+// Agent Module
+//
+// Interactive agent for natural language interaction with robonix system
+
+pub mod agent;
+pub mod functions;
+pub mod llm;
+
+pub use agent::{Agent, AgentRequest, AgentResponse};
+pub use functions::FunctionRegistry;
+pub use llm::AgentConfig;
diff --git a/rust/robonix-core/src/cognition/mod.rs b/rust/robonix-core/src/cognition/mod.rs
new file mode 100644
index 0000000..8e3228e
--- /dev/null
+++ b/rust/robonix-core/src/cognition/mod.rs
@@ -0,0 +1 @@
+pub mod specs;
diff --git a/rust/robonix-core/src/cognition/specs.rs b/rust/robonix-core/src/cognition/specs.rs
new file mode 100644
index 0000000..879ca14
--- /dev/null
+++ b/rust/robonix-core/src/cognition/specs.rs
@@ -0,0 +1,42 @@
+use crate::spec::{PrimitiveSpec, ServiceSpec};
+use std::collections::HashMap;
+
+pub fn load_primitives(primitives: &mut HashMap) {
+ // AMCL pose primitive - provides PoseWithCovarianceStamped directly from AMCL
+ PRM!(primitives, "prm::base.pose.cov", "Get robot pose in map frame from AMCL (PoseWithCovarianceStamped)",
+ {}, // No input parameters
+ { "pose": "geometry_msgs/msg/PoseWithCovarianceStamped" });
+}
+
+pub fn load_services(services: &mut HashMap) {
+ SRV!(
+ services,
+ "srv::task_plan",
+ "Task planning service converting natural language to RTDL",
+ "robonix_sdk/srv/service/task_plan/PlanTask"
+ );
+
+ SRV!(
+ // TODO
+ services,
+ "srv::plan_simulate",
+ "Plan simulation service for feasibility and safety checking",
+ "robonix_sdk/srv/service/plan_simulate/SimulatePlan"
+ );
+
+ SRV!(
+ // TODO
+ services,
+ "srv::memory",
+ "Cognitive memory service providing long-term and short-term knowledge",
+ "robonix_sdk/srv/service/memory/QueryMemory"
+ );
+
+ SRV!(
+ // TODO
+ services,
+ "srv::result_feedback",
+ "Result feedback service for execution verification",
+ "robonix_sdk/srv/service/result_feedback/ResultFeedback"
+ );
+}
diff --git a/rust/robonix-core/src/config.rs b/rust/robonix-core/src/config.rs
new file mode 100644
index 0000000..ac75566
--- /dev/null
+++ b/rust/robonix-core/src/config.rs
@@ -0,0 +1,95 @@
+// SPDX-License-Identifier: MulanPSL-2.0
+// Core Configuration Module
+//
+// Configuration management for robonix-core (distributed system, single instance)
+
+use crate::agent::llm::AgentConfig;
+use anyhow::{Context, Result};
+use dirs;
+use serde::{Deserialize, Serialize};
+use std::path::PathBuf;
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct SpeechConfig {
+ #[serde(default)]
+ pub access_token: String,
+ #[serde(default)]
+ pub appkey: String,
+ #[serde(default = "default_region")]
+ pub region: String,
+}
+
+fn default_region() -> String {
+ "shanghai".to_string()
+}
+
+impl Default for SpeechConfig {
+ fn default() -> Self {
+ Self {
+ access_token: String::new(),
+ appkey: String::new(),
+ region: default_region(),
+ }
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct CoreConfig {
+ #[serde(default)]
+ pub agent: AgentConfig,
+ #[serde(default)]
+ pub speech: SpeechConfig,
+}
+
+impl CoreConfig {
+ /// Get the core config file path
+ /// Uses ~/.robonix/core-config.yaml for core-specific configuration
+ pub fn config_file_path() -> Result {
+ let home_dir = dirs::home_dir().context("Failed to get home directory")?;
+ Ok(home_dir.join(".robonix").join("core-config.yaml"))
+ }
+
+ pub fn load() -> Result {
+ let config_path = Self::config_file_path()?;
+
+ if !config_path.exists() {
+ // Create default config
+ let default = Self::default();
+ default.save()?;
+ return Ok(default);
+ }
+
+ let content = std::fs::read_to_string(&config_path)
+ .with_context(|| format!("Failed to read config file: {}", config_path.display()))?;
+
+ let config: CoreConfig = serde_yaml::from_str(&content)
+ .with_context(|| format!("Failed to parse config file: {}", config_path.display()))?;
+
+ Ok(config)
+ }
+
+ pub fn save(&self) -> Result<()> {
+ let config_path = Self::config_file_path()?;
+
+ // Create parent directory if it doesn't exist
+ if let Some(parent) = config_path.parent() {
+ std::fs::create_dir_all(parent).with_context(|| {
+ format!("Failed to create config directory: {}", parent.display())
+ })?;
+ }
+
+ let content = serde_yaml::to_string(self).context("Failed to serialize config")?;
+
+ std::fs::write(&config_path, content)
+ .with_context(|| format!("Failed to write config file: {}", config_path.display()))?;
+
+ Ok(())
+ }
+
+ pub fn default() -> Self {
+ Self {
+ agent: AgentConfig::default(),
+ speech: SpeechConfig::default(),
+ }
+ }
+}
diff --git a/rust/robonix-core/src/core.rs b/rust/robonix-core/src/core.rs
index 2b38613..82395a5 100644
--- a/rust/robonix-core/src/core.rs
+++ b/rust/robonix-core/src/core.rs
@@ -3,12 +3,14 @@
//
// Core coordination module for robonix-core
+use crate::action::skill_library::SkillLibrary;
use crate::primitive::PrimitiveRegistry;
use crate::service::ServiceRegistry;
-use crate::skill_library::SkillLibrary;
use crate::spec::SpecRegistry;
-use crate::task_manager::TaskManager;
+use crate::task::TaskManager;
+use ros2_client::Node;
use std::sync::Arc;
+use tokio::sync::Mutex;
// robonix Core - coordinates all modules according to robonix architecture
pub struct RobonixCore {
@@ -20,7 +22,7 @@ pub struct RobonixCore {
}
impl RobonixCore {
- pub fn new() -> Self {
+ pub fn new(node: Arc>) -> Self {
// Create shared spec registry (used by both primitive and service registries)
let spec_registry = Arc::new(SpecRegistry::new());
@@ -33,6 +35,7 @@ impl RobonixCore {
skill_library.clone(),
service_registry.clone(),
primitive_registry.clone(),
+ node,
);
Self {
diff --git a/rust/robonix-core/src/lib.rs b/rust/robonix-core/src/lib.rs
index 48cdaf4..451a744 100644
--- a/rust/robonix-core/src/lib.rs
+++ b/rust/robonix-core/src/lib.rs
@@ -5,6 +5,14 @@
#[macro_use]
pub mod spec;
+
+pub mod action;
+pub mod agent;
+pub mod cognition;
+pub mod config;
+pub mod perception;
+pub mod speech;
+
pub mod core;
pub mod logging;
pub mod node;
@@ -12,6 +20,5 @@ pub mod primitive;
pub mod ros_idl;
pub mod server;
pub mod service;
-pub mod skill_library;
-pub mod specs_table;
-pub mod task_manager;
+pub mod task;
+pub mod web;
diff --git a/rust/robonix-core/src/logging.rs b/rust/robonix-core/src/logging.rs
index 6cbe274..8e2452b 100644
--- a/rust/robonix-core/src/logging.rs
+++ b/rust/robonix-core/src/logging.rs
@@ -6,8 +6,21 @@
use ansi_term::{Colour, Style};
use env_logger::{Builder, Env, Target};
use std::io::Write;
+use std::sync::{Arc, OnceLock};
+
+use crate::web::{LogBuffer, LogEntry};
+
+static LOG_BUFFER: OnceLock> = OnceLock::new();
pub fn init_logger() {
+ init_logger_with_buffer(None);
+}
+
+pub fn init_logger_with_buffer(log_buffer: Option>) {
+ if let Some(buffer) = log_buffer {
+ let _ = LOG_BUFFER.set(buffer);
+ }
+
let env = Env::default()
.filter_or("RUST_LOG", "robonix_core=info,rustdds=error")
.write_style_or("RUST_LOG_STYLE", "auto");
@@ -15,40 +28,50 @@ pub fn init_logger() {
Builder::from_env(env)
.target(Target::Stderr)
.format(|buf, record| {
- // Get timestamp in Linux kernel style [seconds.microseconds]
static START: std::sync::OnceLock = std::sync::OnceLock::new();
let start = START.get_or_init(std::time::Instant::now);
let elapsed = start.elapsed();
let secs = elapsed.as_secs();
let micros = elapsed.subsec_micros();
- // Get log level with color
let (level_char, level_color) = match record.level() {
log::Level::Error => ('E', Colour::Red),
log::Level::Warn => ('W', Colour::Yellow),
log::Level::Info => ('I', Colour::Green),
- log::Level::Debug => ('D', Colour::Fixed(8)), // Gray (less prominent)
+ log::Level::Debug => ('D', Colour::Fixed(8)),
log::Level::Trace => ('T', Colour::Purple),
};
- // Get process name
let proc_name = std::env::current_exe()
.ok()
.and_then(|p| p.file_name().map(|n| n.to_string_lossy().into_owned()))
.unwrap_or_else(|| "robonix-core".to_string());
- // Write Linux-style log entry with colors: timestamp procname[pid]: LEVEL message
let timestamp = format!("{}.{:06}", secs, micros);
let proc_info = format!("{}[{}]", proc_name, std::process::id());
- // Info messages are white (default), debug messages are dimmed gray, other levels use their level color
let message = format!("{}", record.args());
let painted_message = match record.level() {
- log::Level::Info => Style::new().paint(message), // White (default)
- log::Level::Debug => Style::new().dimmed().paint(message), // Dimmed gray (less prominent)
- _ => level_color.paint(message),
+ log::Level::Info => Style::new().paint(message.clone()),
+ log::Level::Debug => Style::new().dimmed().paint(message.clone()),
+ _ => level_color.paint(message.clone()),
};
+ if let Some(buffer) = LOG_BUFFER.get() {
+ let level_str = match record.level() {
+ log::Level::Error => "ERROR",
+ log::Level::Warn => "WARN",
+ log::Level::Info => "INFO",
+ log::Level::Debug => "DEBUG",
+ log::Level::Trace => "TRACE",
+ };
+ buffer.add_log(LogEntry {
+ timestamp: timestamp.clone(),
+ level: level_str.to_string(),
+ message: message.clone(),
+ });
+ }
+
write!(
buf,
"{} {}: {} {}\n",
diff --git a/rust/robonix-core/src/main.rs b/rust/robonix-core/src/main.rs
index f14028b..1ebba22 100644
--- a/rust/robonix-core/src/main.rs
+++ b/rust/robonix-core/src/main.rs
@@ -3,43 +3,283 @@
//
// Main entry point for robonix-core service
-use log::info;
+use log::{debug, info, warn};
+use robonix_core::agent::{Agent, AgentConfig as LLMAgentConfig};
use robonix_core::core::RobonixCore;
-use robonix_core::logging::init_logger;
-use robonix_core::node::create_node;
+use robonix_core::logging::init_logger_with_buffer;
+use robonix_core::node::create_nodes;
use robonix_core::server::{create_qos, create_servers, run_servers};
+use robonix_core::web::{
+ LogBuffer, NodeLogState, NodeRegistry, agent_chat_handler, agent_reset_handler,
+ create_web_state, get_config_handler, image_handler, image_topics_handler, index,
+ log_subscriptions_delete, log_subscriptions_get, log_subscriptions_post, logs_handler,
+ node_log_get, node_log_post, node_status_post, nodes_handler, primitives_handler,
+ semantic_map_handler, services_handler, settings_page, skills_handler, status_handler,
+ stt_handler, task_cancel_handler, tasks_handler, tf_tree_handler, topics_handler, tts_handler,
+ update_config_handler,
+};
+use rocket::fs::FileServer;
+use rocket::routes;
+use std::net::TcpListener;
use std::sync::Arc;
+use tokio::sync::Mutex;
fn main() {
- init_logger();
+ // Create log buffer first
+ let log_buffer = Arc::new(LogBuffer::new(1000));
+
+ // Initialize logger with buffer
+ init_logger_with_buffer(Some(log_buffer.clone()));
info!("robonix core starting...");
// Create Tokio runtime for async task execution
- // This must be created before RobonixCore::new() because TaskManager spawns background tasks
let rt = tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime");
- // Initialize core within runtime context (spawns background tasks)
+ // Create two ROS2 nodes on separate contexts: core_api for /rbnx/* servers (ping, register, query, task submit, etc.),
+ // core_task for TaskManager (semantic_map, task_plan, executor) and monitors (TF, topic, image).
+ // Two contexts = two DDS/event loops so a long-running task never blocks API requests.
+ let (api_node, task_node, api_context, task_context) = create_nodes();
+ let _api_context = api_context; // Keep API context alive
+ let _task_context = task_context; // Keep task context alive
+ let api_node_arc = Arc::new(Mutex::new(api_node));
+ let task_node_arc = Arc::new(Mutex::new(task_node));
+ let service_qos = create_qos();
+ info!("robonix core nodes started (core_api, core_task)");
+
+ // Initialize core with task node (TaskManager uses it for semantic_map, task_plan, executor)
let core = rt.block_on(async {
- let core = Arc::new(RobonixCore::new());
+ let core = Arc::new(RobonixCore::new(task_node_arc.clone()));
info!("robonix core initialized");
core
});
- let mut node = create_node();
- let service_qos = create_qos();
- info!("robonix core node started");
+ // Create all /rbnx/* service servers on the API node (keeps API responsive)
+ let servers = rt.block_on(async {
+ let mut node_guard = api_node_arc.lock().await;
+ match create_servers(&mut *node_guard, &service_qos) {
+ Ok(servers) => servers,
+ Err(e) => {
+ eprintln!("failed to create servers: {}", e);
+ std::process::exit(1);
+ }
+ }
+ });
+
+ info!("all robonix modules initialized");
+
+ // Create TF monitor and start monitoring (uses task node)
+ let tf_monitor = Arc::new(robonix_core::perception::tf_monitor::TfMonitor::new());
+
+ // Start TF monitoring in background
+ let tf_monitor_clone = tf_monitor.clone();
+ let node_for_tf = task_node_arc.clone();
+ rt.spawn(async move {
+ let mut node_guard = node_for_tf.lock().await;
+ if let Err(e) = tf_monitor_clone.start_monitoring(&mut *node_guard).await {
+ eprintln!("Failed to start TF monitoring: {}", e);
+ }
+ });
+
+ // Create topic monitor (no periodic task - topics discovered on-demand) (uses task node)
+ let topic_monitor = Arc::new(robonix_core::perception::topic_monitor::TopicMonitor::new());
+ // Initial discovery (synchronous, no spawn needed)
+ let topic_monitor_init = topic_monitor.clone();
+ rt.block_on(async {
+ let mut node_guard = task_node_arc.lock().await;
+ if let Err(e) = topic_monitor_init.start_monitoring(&mut *node_guard).await {
+ eprintln!("Failed to start topic monitoring: {}", e);
+ }
+ });
+
+ // Create image monitor (uses task node)
+ let image_storage_dir = std::path::PathBuf::from("/tmp/robonix_images");
+ let image_monitor = Arc::new(robonix_core::perception::image_monitor::ImageMonitor::new(
+ image_storage_dir,
+ ));
+
+ // Start image monitoring: discover image topics and subscribe to them
+ let image_monitor_for_subscribe = image_monitor.clone();
+ let topic_monitor_for_images = topic_monitor.clone();
+ let node_for_images = task_node_arc.clone();
+ rt.spawn(async move {
+ let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(5));
+ loop {
+ interval.tick().await;
+
+ // Get all topics from topic monitor
+ let topics = topic_monitor_for_images.get_topics().await;
+
+ // Find image topics (sensor_msgs/msg/Image)
+ for topic in topics.topics {
+ if topic.message_type.contains("sensor_msgs/msg/Image")
+ || topic.message_type.contains("Image")
+ {
+ // Subscribe to image topic
+ let mut node_guard = node_for_images.lock().await;
+ if let Err(e) = image_monitor_for_subscribe
+ .register_image_topic(
+ &mut *node_guard,
+ topic.name.clone(),
+ topic.message_type.clone(),
+ )
+ .await
+ {
+ debug!("Failed to subscribe to image topic {}: {}", topic.name, e);
+ }
+ }
+ }
+ }
+ });
+
+ // Check if both web environment variables are set
+ let web_dir_opt = std::env::var("ROBONIX_WEB_ASSETS_DIR").ok();
+ let port_opt = std::env::var("ROBONIX_WEB_PORT")
+ .ok()
+ .and_then(|p| p.parse::().ok());
- let servers = match create_servers(&mut node, &service_qos) {
- Ok(servers) => servers,
- Err(e) => {
- eprintln!("failed to create servers: {}", e);
+ // Only start web server if both environment variables are set
+ if let (Some(web_dir_str), Some(base_port)) = (web_dir_opt, port_opt) {
+ let web_dir = std::path::PathBuf::from(web_dir_str);
+
+ // Verify web directory exists
+ if !web_dir.exists() || !web_dir.is_dir() {
+ eprintln!(
+ "Error: Web directory does not exist or is not a directory: {:?}",
+ web_dir
+ );
+ eprintln!(
+ "Please set ROBONIX_WEB_ASSETS_DIR to the web directory path (e.g., /path/to/robonix-core/web)."
+ );
std::process::exit(1);
}
- };
- info!("all robonix modules initialized");
- info!("robonix core ready. waiting for requests...");
+ // Verify static subdirectory exists
+ let static_dir = web_dir.join("static");
+ if !static_dir.exists() || !static_dir.is_dir() {
+ eprintln!(
+ "Error: Static directory does not exist or is not a directory: {:?}",
+ static_dir
+ );
+ eprintln!("Please ensure the web directory contains a 'static' subdirectory.");
+ std::process::exit(1);
+ }
+
+ // Check if the specified port is available
+ let port = if TcpListener::bind(("0.0.0.0", base_port)).is_ok() {
+ base_port
+ } else {
+ eprintln!(
+ "Error: Port {} is already in use. Please free the port or set ROBONIX_WEB_PORT to a different port.",
+ base_port
+ );
+ std::process::exit(1);
+ };
+
+ // Load config and create services
+ let (agent_config, speech_config) = rt.block_on(async {
+ use robonix_core::config::CoreConfig;
+ match CoreConfig::load() {
+ Ok(config) => (config.agent, config.speech),
+ Err(e) => {
+ warn!("Failed to load core config, using defaults: {}", e);
+ (
+ LLMAgentConfig::default(),
+ robonix_core::config::SpeechConfig::default(),
+ )
+ }
+ }
+ });
+
+ let agent = Arc::new(tokio::sync::Mutex::new(Agent::new(
+ core.clone(),
+ agent_config,
+ image_monitor.clone(),
+ )));
+
+ // Create TTS and STT services
+ let tts_service = Arc::new(robonix_core::speech::TtsService::new(speech_config.clone()));
+ let stt_service = Arc::new(robonix_core::speech::SttService::new(speech_config));
+
+ let node_log_state = Arc::new(NodeLogState::new());
+ let node_registry = Arc::new(NodeRegistry::new());
+
+ // Create web state (node ref is for monitors; use task node)
+ let web_state = create_web_state(
+ core.clone(),
+ task_node_arc.clone(),
+ tf_monitor.clone(),
+ topic_monitor.clone(),
+ log_buffer.clone(),
+ image_monitor.clone(),
+ agent,
+ tts_service,
+ stt_service,
+ web_dir.clone(),
+ node_log_state,
+ node_registry,
+ );
+
+ info!("starting web server on http://localhost:{}", port);
+
+ // Start Rocket web server in a separate task
+ let web_state_clone = web_state.clone();
+ let static_dir_clone = static_dir.clone();
+ let port_for_log = port;
+ rt.spawn(async move {
+ let config = rocket::Config::figment()
+ .merge(("port", port))
+ .merge(("address", "0.0.0.0"));
+
+ let result = rocket::custom(config)
+ .manage(web_state_clone)
+ .mount(
+ "/",
+ routes![
+ index,
+ settings_page,
+ status_handler,
+ tf_tree_handler,
+ topics_handler,
+ tasks_handler,
+ task_cancel_handler,
+ skills_handler,
+ services_handler,
+ primitives_handler,
+ logs_handler,
+ log_subscriptions_post,
+ log_subscriptions_delete,
+ log_subscriptions_get,
+ node_log_post,
+ node_log_get,
+ node_status_post,
+ nodes_handler,
+ semantic_map_handler,
+ image_topics_handler,
+ image_handler,
+ get_config_handler,
+ update_config_handler,
+ agent_chat_handler,
+ agent_reset_handler,
+ tts_handler,
+ stt_handler,
+ ],
+ )
+ .mount("/static", FileServer::from(static_dir_clone))
+ .launch()
+ .await;
+
+ if let Err(e) = result {
+ eprintln!("Web server error: {}", e);
+ }
+ });
+
+ info!("robonix core ready. waiting for requests...");
+ info!("web available at http://localhost:{}", port_for_log);
+ } else {
+ info!("robonix core ready. waiting for requests...");
+ info!("web disabled (set ROBONIX_WEB_ASSETS_DIR and ROBONIX_WEB_PORT to enable)");
+ }
// Run servers in Tokio runtime
rt.block_on(run_servers(servers, core));
diff --git a/rust/robonix-core/src/node.rs b/rust/robonix-core/src/node.rs
index 4eaed17..a2fb8c0 100644
--- a/rust/robonix-core/src/node.rs
+++ b/rust/robonix-core/src/node.rs
@@ -4,13 +4,26 @@
// ROS2 node creation utilities
use ros2_client::{Context, Node, NodeName, NodeOptions};
+use std::sync::Arc;
-pub fn create_node() -> Node {
- let context = Context::new().unwrap();
- context
+/// Create two ROS2 nodes on **separate contexts**: one for core API servers (ping, register, query, task submit, etc.),
+/// one for task runtime (semantic_map client, task_plan client, executor) and monitors (TF, topic, image).
+/// Two contexts give two independent DDS/event loops so a long-running task (e.g. semantic map update) never blocks API requests.
+/// Returns (api_node, task_node, api_context, task_context). Both contexts must be kept alive.
+pub fn create_nodes() -> (Node, Node, Arc, Arc) {
+ let api_context = Arc::new(Context::new().unwrap());
+ let api_node = api_context
.new_node(
- NodeName::new("/rbnx", "core").unwrap(),
+ NodeName::new("/rbnx", "core_api").unwrap(),
NodeOptions::new().enable_rosout(true),
)
- .unwrap()
+ .unwrap();
+ let task_context = Arc::new(Context::new().unwrap());
+ let task_node = task_context
+ .new_node(
+ NodeName::new("/rbnx", "core_task").unwrap(),
+ NodeOptions::new().enable_rosout(true),
+ )
+ .unwrap();
+ (api_node, task_node, api_context, task_context)
}
diff --git a/rust/robonix-core/src/perception/image_monitor.rs b/rust/robonix-core/src/perception/image_monitor.rs
new file mode 100644
index 0000000..5ca2ccc
--- /dev/null
+++ b/rust/robonix-core/src/perception/image_monitor.rs
@@ -0,0 +1,758 @@
+// SPDX-License-Identifier: MulanPSL-2.0
+// Image Monitor Module
+//
+// Monitors image topics and stores latest images for web UI display
+
+use futures_util::stream::StreamExt;
+use log::{info, trace, warn};
+use ros2_client::{
+ Message, MessageTypeName, Name, Node, Subscription,
+ rustdds::{
+ QosPolicyBuilder,
+ policy::{self, Reliability},
+ },
+};
+use serde::{Deserialize, Serialize};
+use std::collections::HashMap;
+use std::path::{Path, PathBuf};
+use std::sync::Arc;
+use std::time::SystemTime;
+use tokio::fs;
+use tokio::sync::Mutex;
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ImageTopicInfo {
+ pub topic_name: String,
+ pub message_type: String,
+ /// ROS image encoding (e.g. rgb8, bgr8, mono8). Set when first image is received.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub encoding: Option,
+ pub last_update: Option,
+ pub image_paths: Vec, // Paths to recent images (up to 10, sorted by time, newest first)
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ImagePathInfo {
+ pub path: String,
+ pub timestamp: u64, // Unix timestamp in seconds
+}
+
+// sensor_msgs/msg/Image message structure
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct Header {
+ pub stamp: Stamp,
+ pub frame_id: String,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct Stamp {
+ pub sec: i32,
+ pub nanosec: u32,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ImageMsg {
+ pub header: Header,
+ pub height: u32,
+ pub width: u32,
+ pub encoding: String,
+ pub is_bigendian: u8,
+ pub step: u32,
+ pub data: Vec,
+}
+
+impl Message for ImageMsg {}
+
+pub struct ImageMonitor {
+ image_topics: Arc>>,
+ image_storage_dir: PathBuf,
+ subscriptions: Arc>>>>,
+}
+
+impl ImageMonitor {
+ pub fn new(storage_dir: PathBuf) -> Self {
+ // Create storage directory if it doesn't exist
+ if let Err(e) = std::fs::create_dir_all(&storage_dir) {
+ warn!(
+ "Failed to create image storage directory {:?}: {}",
+ storage_dir, e
+ );
+ }
+
+ Self {
+ image_topics: Arc::new(Mutex::new(HashMap::new())),
+ image_storage_dir: storage_dir,
+ subscriptions: Arc::new(Mutex::new(HashMap::new())),
+ }
+ }
+
+ /// Get all image topics
+ pub async fn get_image_topics(&self) -> Vec {
+ let topics = self.image_topics.lock().await;
+ topics.values().cloned().collect()
+ }
+
+ /// Returns true if the encoding is a color (RGB/BGR) encoding suitable for VLM.
+ fn is_rgb_encoding(encoding: &str) -> bool {
+ let enc = encoding.to_lowercase();
+ matches!(
+ enc.as_str(),
+ "rgb8" | "bgr8" | "rgba8" | "bgra8" | "8uc3" | "8uc4"
+ )
+ }
+
+ /// Get full filesystem paths of the latest image for each RGB topic.
+ /// Used by the agent's describe_robot_vision to send images to the VLM.
+ pub async fn get_rgb_image_paths(&self) -> Vec {
+ let topics = self.image_topics.lock().await;
+ let mut paths = Vec::new();
+ for info in topics.values() {
+ let encoding = info.encoding.as_deref().unwrap_or("");
+ if !Self::is_rgb_encoding(encoding) {
+ continue;
+ }
+ if let Some(latest) = info.image_paths.first() {
+ // Stored path is "images/xxx.jpg"; file on disk is storage_dir/xxx.jpg
+ let filename = latest
+ .path
+ .strip_prefix("images/")
+ .unwrap_or(latest.path.as_str());
+ paths.push(self.image_storage_dir.join(filename));
+ }
+ }
+ paths
+ }
+
+ /// Update image topic info (called when a new image is received)
+ pub async fn update_image(&self, topic_name: String, image_path: String) {
+ let mut topics = self.image_topics.lock().await;
+ let timestamp = SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .unwrap()
+ .as_secs();
+
+ if let Some(info) = topics.get_mut(&topic_name) {
+ info.last_update = Some(SystemTime::now());
+ // Replace with latest image (only keep one)
+ info.image_paths = vec![ImagePathInfo {
+ path: image_path,
+ timestamp,
+ }];
+ // encoding is set in process_image_message_internal
+ } else {
+ // New topic, add it (encoding will be set when first image is processed)
+ topics.insert(
+ topic_name.clone(),
+ ImageTopicInfo {
+ topic_name: topic_name.clone(),
+ message_type: "sensor_msgs/msg/Image".to_string(),
+ encoding: None,
+ last_update: Some(SystemTime::now()),
+ image_paths: vec![ImagePathInfo {
+ path: image_path,
+ timestamp,
+ }],
+ },
+ );
+ }
+ }
+
+ /// Register an image topic and start subscribing (called when topic is discovered)
+ pub async fn register_image_topic(
+ &self,
+ node: &mut Node,
+ topic_name: String,
+ message_type: String,
+ ) -> Result<(), Box> {
+ let mut subscriptions = self.subscriptions.lock().await;
+ if subscriptions.contains_key(&topic_name) {
+ // Already subscribed
+ return Ok(());
+ }
+
+ // Register in topics map
+ let mut topics = self.image_topics.lock().await;
+ if !topics.contains_key(&topic_name) {
+ topics.insert(
+ topic_name.clone(),
+ ImageTopicInfo {
+ topic_name: topic_name.clone(),
+ message_type: message_type.clone(),
+ encoding: None,
+ last_update: None,
+ image_paths: Vec::new(),
+ },
+ );
+ }
+ drop(topics);
+
+ // Create QoS for image topics
+ let image_qos = QosPolicyBuilder::new()
+ .history(policy::History::KeepLast { depth: 1 })
+ .reliability(Reliability::BestEffort)
+ .durability(policy::Durability::Volatile)
+ .build();
+
+ // Parse topic name (format: "/namespace/topic_name")
+ let topic_name_parts: Vec<&str> = topic_name.split('/').filter(|s| !s.is_empty()).collect();
+ let namespace = if topic_name_parts.len() > 1 {
+ format!(
+ "/{}",
+ topic_name_parts[..topic_name_parts.len() - 1].join("/")
+ )
+ } else {
+ "/".to_string()
+ };
+ let topic_part = topic_name_parts
+ .last()
+ .map(|s| s.to_string())
+ .unwrap_or_else(|| topic_name.clone());
+
+ // Create topic
+ let image_topic = node.create_topic(
+ &Name::new(&namespace, &topic_part)?,
+ MessageTypeName::new("sensor_msgs", "Image"),
+ &image_qos,
+ )?;
+
+ // Subscribe to topic
+ let subscription: Subscription = node.create_subscription(&image_topic, None)?;
+ let subscription_arc = Arc::new(subscription);
+ subscriptions.insert(topic_name.clone(), subscription_arc.clone());
+ drop(subscriptions);
+
+ // Spawn task to handle image messages
+ let image_topics_clone = self.image_topics.clone();
+ let image_storage_dir_clone = self.image_storage_dir.clone();
+ let topic_name_clone = topic_name.clone();
+ tokio::spawn(async move {
+ trace!("Subscribed to image topic: {}", topic_name_clone);
+ let mut stream = Box::pin(subscription_arc.async_stream());
+ let mut message_count = 0;
+ while let Some(result) = stream.next().await {
+ match result {
+ Ok((msg, _msg_info)) => {
+ message_count += 1;
+ if message_count <= 5 || message_count % 100 == 0 {
+ trace!(
+ "Received image from {}: {}x{}, encoding={}, data_len={}",
+ topic_name_clone,
+ msg.width,
+ msg.height,
+ msg.encoding,
+ msg.data.len()
+ );
+ }
+
+ // Convert image to JPEG and save
+ if let Err(e) = Self::process_image_message_internal(
+ &image_topics_clone,
+ &image_storage_dir_clone,
+ &topic_name_clone,
+ &msg,
+ )
+ .await
+ {
+ trace!("Failed to process image from {}: {}", topic_name_clone, e);
+ }
+ }
+ Err(e) => {
+ trace!("Error receiving image from {}: {:?}", topic_name_clone, e);
+ }
+ }
+ }
+ });
+
+ info!("Registered and subscribed to image topic: {}", topic_name);
+ Ok(())
+ }
+
+ /// Process received image message: convert to JPEG and save (internal static method)
+ async fn process_image_message_internal(
+ image_topics: &Arc>>,
+ image_storage_dir: &PathBuf,
+ topic_name: &str,
+ msg: &ImageMsg,
+ ) -> Result<(), String> {
+ trace!(
+ "Processing image from {}: {}x{}, encoding={}, data_len={}, step={}",
+ topic_name,
+ msg.width,
+ msg.height,
+ msg.encoding,
+ msg.data.len(),
+ msg.step
+ );
+
+ // Convert sensor_msgs/Image raw data to JPEG
+ // The data field contains raw pixel data, not JPEG
+ let jpeg_data = match Self::convert_image_to_jpeg(msg) {
+ Ok(data) => {
+ trace!(
+ "Successfully converted image to JPEG, size: {} bytes",
+ data.len()
+ );
+ data
+ }
+ Err(e) => {
+ trace!(
+ "Failed to convert image to JPEG: {}, saving raw data instead",
+ e
+ );
+ // Fallback: save raw data (may not work in browser)
+ msg.data.clone()
+ }
+ };
+
+ // Save image file
+ let sanitized_name = topic_name.replace('/', "_").replace("~", "_");
+ let timestamp = SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .unwrap()
+ .as_secs();
+ let filename = format!("{}_{}.jpg", sanitized_name, timestamp);
+ let filepath = image_storage_dir.join(&filename);
+
+ trace!("Saving image to: {:?}", filepath);
+ fs::write(&filepath, &jpeg_data).await.map_err(|e| {
+ let err_msg = format!("Failed to write image file {:?}: {}", filepath, e);
+ trace!("{}", err_msg);
+ err_msg
+ })?;
+
+ // Verify file was written
+ if let Ok(metadata) = fs::metadata(&filepath).await {
+ trace!(
+ "Image file saved successfully: {:?}, size: {} bytes",
+ filepath,
+ metadata.len()
+ );
+ } else {
+ trace!(
+ "Warning: Could not verify image file after write: {:?}",
+ filepath
+ );
+ }
+
+ let image_path = format!("images/{}", filename);
+ trace!("Image path for API: {}", image_path);
+
+ // Update image topic info (only keep latest one)
+ let mut topics = image_topics.lock().await;
+ let timestamp_secs = SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .unwrap()
+ .as_secs();
+
+ if let Some(info) = topics.get_mut(topic_name) {
+ info.last_update = Some(SystemTime::now());
+ info.encoding = Some(msg.encoding.clone());
+ // Replace with latest image (only keep one)
+ info.image_paths = vec![ImagePathInfo {
+ path: image_path.clone(),
+ timestamp: timestamp_secs,
+ }];
+ trace!(
+ "Updated image topic {} with new image: {}",
+ topic_name, image_path
+ );
+ } else {
+ topics.insert(
+ topic_name.to_string(),
+ ImageTopicInfo {
+ topic_name: topic_name.to_string(),
+ message_type: "sensor_msgs/msg/Image".to_string(),
+ encoding: Some(msg.encoding.clone()),
+ last_update: Some(SystemTime::now()),
+ image_paths: vec![ImagePathInfo {
+ path: image_path.clone(),
+ timestamp: timestamp_secs,
+ }],
+ },
+ );
+ trace!("Created new image topic entry: {}", topic_name);
+ }
+
+ Ok(())
+ }
+
+ /// Convert sensor_msgs/Image raw data to JPEG format
+ fn convert_image_to_jpeg(msg: &ImageMsg) -> Result, String> {
+ use image::{ImageBuffer, Rgb, RgbImage};
+
+ let width = msg.width;
+ let height = msg.height;
+ let step = msg.step as usize;
+
+ trace!(
+ "Converting image: {}x{}, encoding={}, step={}, data_len={}",
+ width,
+ height,
+ msg.encoding,
+ step,
+ msg.data.len()
+ );
+
+ // Handle different encodings
+ // Check encoding (case-insensitive, handle variations)
+ let encoding_lower = msg.encoding.to_lowercase();
+ match encoding_lower.as_str() {
+ "rgb8" | "bgr8" | "8uc3" => {
+ // RGB8 or BGR8: 3 bytes per pixel
+ let expected_size = (width as usize) * (height as usize) * 3;
+ if msg.data.len() < expected_size {
+ return Err(format!(
+ "Image data too short: expected {}, got {}",
+ expected_size,
+ msg.data.len()
+ ));
+ }
+
+ let mut img: RgbImage = ImageBuffer::new(width, height);
+
+ for y in 0..height {
+ for x in 0..width {
+ let idx = (y as usize) * step + (x as usize) * 3;
+ if idx + 2 < msg.data.len() {
+ let r = msg.data[idx];
+ let g = msg.data[idx + 1];
+ let b = msg.data[idx + 2];
+
+ // Handle BGR vs RGB (case-insensitive)
+ let (r, g, b) = if encoding_lower == "bgr8" || encoding_lower == "8uc3"
+ {
+ (b, g, r) // Swap R and B for BGR
+ } else {
+ (r, g, b)
+ };
+
+ img.put_pixel(x, y, Rgb([r, g, b]));
+ }
+ }
+ }
+
+ // Encode to JPEG
+ let mut buffer = Vec::new();
+ let mut cursor = std::io::Cursor::new(&mut buffer);
+ img.write_to(&mut cursor, image::ImageFormat::Jpeg)
+ .map_err(|e| format!("Failed to encode JPEG: {}", e))?;
+
+ Ok(buffer)
+ }
+ "rgba8" | "bgra8" | "8uc4" => {
+ // RGBA8 or BGRA8: 4 bytes per pixel (R, G, B, A)
+ // We'll ignore the alpha channel and convert to RGB
+ let expected_size = (width as usize) * (height as usize) * 4;
+ if msg.data.len() < expected_size {
+ return Err(format!(
+ "Image data too short: expected {}, got {}",
+ expected_size,
+ msg.data.len()
+ ));
+ }
+
+ let mut img: RgbImage = ImageBuffer::new(width, height);
+
+ for y in 0..height {
+ for x in 0..width {
+ let idx = (y as usize) * step + (x as usize) * 4;
+ if idx + 3 < msg.data.len() {
+ let r = msg.data[idx];
+ let g = msg.data[idx + 1];
+ let b = msg.data[idx + 2];
+ // Alpha channel at idx + 3, we ignore it
+
+ // Handle BGRA vs RGBA (case-insensitive)
+ let (r, g, b) = if encoding_lower == "bgra8" || encoding_lower == "8uc4"
+ {
+ (b, g, r) // Swap R and B for BGRA
+ } else {
+ (r, g, b)
+ };
+
+ img.put_pixel(x, y, Rgb([r, g, b]));
+ }
+ }
+ }
+
+ // Encode to JPEG
+ let mut buffer = Vec::new();
+ let mut cursor = std::io::Cursor::new(&mut buffer);
+ img.write_to(&mut cursor, image::ImageFormat::Jpeg)
+ .map_err(|e| format!("Failed to encode JPEG: {}", e))?;
+
+ Ok(buffer)
+ }
+ "mono8" => {
+ // Grayscale: 1 byte per pixel
+ let expected_size = (width as usize) * (height as usize);
+ if msg.data.len() < expected_size {
+ return Err(format!(
+ "Image data too short: expected {}, got {}",
+ expected_size,
+ msg.data.len()
+ ));
+ }
+
+ let mut img: ImageBuffer, Vec> = ImageBuffer::new(width, height);
+
+ for y in 0..height {
+ for x in 0..width {
+ let idx = (y as usize) * step + (x as usize);
+ if idx < msg.data.len() {
+ let gray = msg.data[idx];
+ img.put_pixel(x, y, Rgb([gray, gray, gray]));
+ }
+ }
+ }
+
+ // Encode to JPEG
+ let mut buffer = Vec::new();
+ let mut cursor = std::io::Cursor::new(&mut buffer);
+ img.write_to(&mut cursor, image::ImageFormat::Jpeg)
+ .map_err(|e| format!("Failed to encode JPEG: {}", e))?;
+
+ Ok(buffer)
+ }
+ "16uc1" | "mono16" => {
+ // Depth image: 16-bit unsigned integer, 1 channel (2 bytes per pixel)
+ // Convert to normalized 8-bit grayscale for visualization
+ use byteorder::{BigEndian, ByteOrder, LittleEndian};
+
+ let expected_size = (width as usize) * (height as usize) * 2;
+ if msg.data.len() < expected_size {
+ return Err(format!(
+ "Image data too short: expected {}, got {}",
+ expected_size,
+ msg.data.len()
+ ));
+ }
+
+ let mut img: ImageBuffer, Vec> = ImageBuffer::new(width, height);
+ let is_bigendian = msg.is_bigendian != 0;
+
+ // Find min and max for normalization (excluding 0 which is invalid depth)
+ let mut min_val = u16::MAX;
+ let mut max_val = 0u16;
+
+ for y in 0..height {
+ for x in 0..width {
+ let idx = (y as usize) * step + (x as usize) * 2;
+ if idx + 1 < msg.data.len() {
+ let val = if is_bigendian {
+ BigEndian::read_u16(&msg.data[idx..idx + 2])
+ } else {
+ LittleEndian::read_u16(&msg.data[idx..idx + 2])
+ };
+ if val > 0 && val < min_val {
+ min_val = val;
+ }
+ if val > max_val {
+ max_val = val;
+ }
+ }
+ }
+ }
+
+ // Normalize and convert to RGB
+ let range = if max_val > min_val {
+ max_val - min_val
+ } else {
+ 1
+ };
+ for y in 0..height {
+ for x in 0..width {
+ let idx = (y as usize) * step + (x as usize) * 2;
+ if idx + 1 < msg.data.len() {
+ let val = if is_bigendian {
+ BigEndian::read_u16(&msg.data[idx..idx + 2])
+ } else {
+ LittleEndian::read_u16(&msg.data[idx..idx + 2])
+ };
+
+ // Normalize to 0-255, with 0 (invalid depth) as black
+ let normalized = if val == 0 {
+ 0u8
+ } else {
+ (((val - min_val) as f32 / range as f32) * 255.0) as u8
+ };
+
+ // Use grayscale visualization for depth
+ img.put_pixel(x, y, Rgb([normalized, normalized, normalized]));
+ }
+ }
+ }
+
+ // Encode to JPEG
+ let mut buffer = Vec::new();
+ let mut cursor = std::io::Cursor::new(&mut buffer);
+ img.write_to(&mut cursor, image::ImageFormat::Jpeg)
+ .map_err(|e| format!("Failed to encode JPEG: {}", e))?;
+
+ Ok(buffer)
+ }
+ "32fc1" => {
+ // Depth image: 32-bit float, 1 channel (4 bytes per pixel)
+ // Convert to normalized 8-bit grayscale for visualization
+ use byteorder::{BigEndian, ByteOrder, LittleEndian};
+
+ let expected_size = (width as usize) * (height as usize) * 4;
+ if msg.data.len() < expected_size {
+ return Err(format!(
+ "Image data too short: expected {}, got {}",
+ expected_size,
+ msg.data.len()
+ ));
+ }
+
+ let mut img: ImageBuffer, Vec> = ImageBuffer::new(width, height);
+ let is_bigendian = msg.is_bigendian != 0;
+
+ // Find min and max for normalization (excluding NaN and Inf)
+ let mut min_val = f32::MAX;
+ let mut max_val = f32::MIN;
+
+ for y in 0..height {
+ for x in 0..width {
+ let idx = (y as usize) * step + (x as usize) * 4;
+ if idx + 3 < msg.data.len() {
+ let val = if is_bigendian {
+ BigEndian::read_f32(&msg.data[idx..idx + 4])
+ } else {
+ LittleEndian::read_f32(&msg.data[idx..idx + 4])
+ };
+ if val.is_finite() && val > 0.0 {
+ if val < min_val {
+ min_val = val;
+ }
+ if val > max_val {
+ max_val = val;
+ }
+ }
+ }
+ }
+ }
+
+ // Normalize and convert to RGB
+ let range = if max_val > min_val {
+ max_val - min_val
+ } else {
+ 1.0
+ };
+ for y in 0..height {
+ for x in 0..width {
+ let idx = (y as usize) * step + (x as usize) * 4;
+ if idx + 3 < msg.data.len() {
+ let val = if is_bigendian {
+ BigEndian::read_f32(&msg.data[idx..idx + 4])
+ } else {
+ LittleEndian::read_f32(&msg.data[idx..idx + 4])
+ };
+
+ // Normalize to 0-255, with NaN/Inf/zero as black
+ let normalized = if !val.is_finite() || val <= 0.0 {
+ 0u8
+ } else {
+ (((val - min_val) / range) * 255.0).clamp(0.0, 255.0) as u8
+ };
+
+ // Use grayscale visualization for depth
+ img.put_pixel(x, y, Rgb([normalized, normalized, normalized]));
+ }
+ }
+ }
+
+ // Encode to JPEG
+ let mut buffer = Vec::new();
+ let mut cursor = std::io::Cursor::new(&mut buffer);
+ img.write_to(&mut cursor, image::ImageFormat::Jpeg)
+ .map_err(|e| format!("Failed to encode JPEG: {}", e))?;
+
+ Ok(buffer)
+ }
+ _ => {
+ trace!(
+ "Unsupported encoding: {}, cannot convert to JPEG",
+ msg.encoding
+ );
+ Err(format!(
+ "Unsupported encoding: {} (supported: rgb8, bgr8, rgba8, bgra8, mono8, 16UC1, mono16, 32FC1)",
+ msg.encoding
+ ))
+ }
+ }
+ }
+
+ /// Get storage directory path
+ pub fn get_storage_dir(&self) -> &Path {
+ &self.image_storage_dir
+ }
+
+ /// Save image data to file and return relative path
+ pub async fn save_image(&self, topic_name: &str, image_data: &[u8]) -> Result {
+ // Sanitize topic name for filename
+ let sanitized_name = topic_name.replace('/', "_").replace("~", "_");
+
+ // Create filename with timestamp
+ let timestamp = SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .unwrap()
+ .as_secs();
+ let filename = format!("{}_{}.jpg", sanitized_name, timestamp);
+ let filepath = self.image_storage_dir.join(&filename);
+
+ // Write image data
+ fs::write(&filepath, image_data)
+ .await
+ .map_err(|e| format!("Failed to write image file: {}", e))?;
+
+ // Remove old images for this topic (keep only last 1)
+ self.cleanup_old_images(&sanitized_name, 1).await;
+
+ // Return relative path for API
+ Ok(format!("images/{}", filename))
+ }
+
+ /// Clean up old images for a topic, keeping only the last N images
+ async fn cleanup_old_images(&self, sanitized_topic_name: &str, keep_count: usize) {
+ // Use blocking I/O for directory reading (simpler and acceptable for cleanup)
+ let storage_dir = self.image_storage_dir.clone();
+ let topic_name = sanitized_topic_name.to_string();
+
+ tokio::task::spawn_blocking(move || {
+ if let Ok(entries) = std::fs::read_dir(&storage_dir) {
+ let mut entry_vec: Vec<(std::path::PathBuf, Option)> =
+ entries
+ .filter_map(|e| e.ok())
+ .filter_map(|e| {
+ let path = e.path();
+ if path
+ .file_name()
+ .and_then(|n| n.to_str())
+ .map(|s| s.starts_with(&topic_name))
+ .unwrap_or(false)
+ {
+ let metadata = std::fs::metadata(&path).ok();
+ let modified = metadata.and_then(|m| m.modified().ok());
+ Some((path, modified))
+ } else {
+ None
+ }
+ })
+ .collect();
+
+ // Sort by modified time (newest first)
+ entry_vec.sort_by(|a, b| b.1.cmp(&a.1));
+
+ // Remove all but the last N images
+ for (path, _) in entry_vec.into_iter().skip(keep_count) {
+ if let Err(e) = std::fs::remove_file(&path) {
+ trace!("Failed to remove old image file: {}", e);
+ }
+ }
+ }
+ })
+ .await
+ .ok();
+ }
+}
diff --git a/rust/robonix-core/src/perception/mod.rs b/rust/robonix-core/src/perception/mod.rs
new file mode 100644
index 0000000..aa41ccc
--- /dev/null
+++ b/rust/robonix-core/src/perception/mod.rs
@@ -0,0 +1,4 @@
+pub mod image_monitor;
+pub mod specs;
+pub mod tf_monitor;
+pub mod topic_monitor;
diff --git a/rust/robonix-core/src/perception/specs.rs b/rust/robonix-core/src/perception/specs.rs
new file mode 100644
index 0000000..f03171d
--- /dev/null
+++ b/rust/robonix-core/src/perception/specs.rs
@@ -0,0 +1,59 @@
+use crate::spec::{PrimitiveSpec, ServiceSpec};
+use std::collections::HashMap;
+
+pub fn load_primitives(primitives: &mut HashMap) {
+ PRM!(primitives, "prm::camera.rgb", "Capture RGB image from camera",
+ {}, // No input parameters
+ { "image": "sensor_msgs/msg/Image" });
+
+ PRM!(primitives, "prm::lidar.scan", "Scan environment with lidar",
+ {}, // No input parameters
+ { "scan": "sensor_msgs/msg/LaserScan" });
+
+ PRM!(primitives, "prm::camera.depth", "Capture depth image from depth camera",
+ {}, // No input parameters
+ { "depth": "sensor_msgs/msg/Image" });
+
+ PRM!(primitives, "prm::sensor.pointcloud", "Capture point cloud data from 3D sensor",
+ {}, // No input parameters
+ { "pointcloud": "sensor_msgs/msg/PointCloud2" });
+
+ PRM!(primitives, "prm::camera.rgbd", "Capture RGB and depth images from RGBD camera",
+ {}, // No input parameters
+ { "rgb": "sensor_msgs/msg/Image", "depth": "sensor_msgs/msg/Image" });
+
+ PRM!(primitives, "prm::trasform.laserscan", "Transform point cloud to laser scan",
+ { "pointcloud": "sensor_msgs/msg/PointCloud2" },
+ { "scan": "sensor_msgs/msg/LaserScan" });
+
+ PRM!(primitives, "prm::description.urdf", "Provide robot URDF description",
+ {}, // No input parameters
+ { "urdf": "std_msgs/msg/String" }); // robot_description parameter as string
+
+ PRM!(primitives, "prm::slam.vision", "Visual SLAM providing map and localization",
+ {}, // No input parameters (subscribes to RGB, depth, pointcloud internally)
+ { "map": "nav_msgs/msg/OccupancyGrid" }); // Output map
+}
+
+pub fn load_services(services: &mut HashMap) {
+ SRV!(
+ services,
+ "srv::spatial_map",
+ "Spatial map service providing geometric structure information",
+ "robonix_sdk/srv/service/spatial_map/GetSpatialMap"
+ );
+
+ SRV!(
+ services,
+ "srv::semantic_map",
+ "Semantic map service providing entity-level representation using VLM for object detection and depth camera for accurate distance measurement",
+ "robonix_sdk/srv/service/semantic_map/QuerySemanticMap"
+ );
+
+ SRV!(
+ services,
+ "srv::transform.scan",
+ "Transform point cloud to laser scan. Serves many callers: CONVERT (one shot), START_STREAM (per-client output_topic), STOP_STREAM (release).",
+ "robonix_sdk/srv/service/transform_scan/TransformScan"
+ );
+}
diff --git a/rust/robonix-core/src/perception/tf_monitor.rs b/rust/robonix-core/src/perception/tf_monitor.rs
new file mode 100644
index 0000000..f31cde4
--- /dev/null
+++ b/rust/robonix-core/src/perception/tf_monitor.rs
@@ -0,0 +1,405 @@
+// SPDX-License-Identifier: MulanPSL-2.0
+// TF Monitor Module
+//
+// Monitors and parses ROS2 TF tree data
+
+use futures_util::stream::StreamExt;
+use log::debug;
+use ros2_client::{
+ Message, MessageTypeName, Name, Node, Subscription,
+ rustdds::{
+ Duration, QosPolicyBuilder,
+ policy::{self, Reliability},
+ },
+};
+use serde::{Deserialize, Serialize};
+use std::collections::HashMap;
+use std::sync::Arc;
+use tokio::sync::Mutex;
+
+// TF2 message types
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct Header {
+ pub stamp: Stamp,
+ pub frame_id: String,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct Stamp {
+ pub sec: i32,
+ pub nanosec: u32,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct Transform {
+ pub translation: Vector3,
+ pub rotation: Quaternion,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct Vector3 {
+ pub x: f64,
+ pub y: f64,
+ pub z: f64,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct Quaternion {
+ pub x: f64,
+ pub y: f64,
+ pub z: f64,
+ pub w: f64,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct TransformStamped {
+ pub header: Header,
+ pub child_frame_id: String,
+ pub transform: Transform,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct TFMessage {
+ pub transforms: Vec,
+}
+
+impl Message for TFMessage {}
+
+#[derive(Serialize, Deserialize, Clone, Debug)]
+pub struct TfFrame {
+ pub frame_id: String,
+ pub child_frames: Vec,
+ pub parent_frame: Option,
+ pub transform: Option,
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug)]
+pub struct TfTransform {
+ pub translation: [f64; 3],
+ pub rotation: [f64; 4], // quaternion [x, y, z, w]
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct TfTreeResponse {
+ pub frames: Vec,
+ pub root_frames: Vec,
+}
+
+pub struct TfTreeCache {
+ pub frames: HashMap,
+ pub frame_last_seen: HashMap,
+ pub static_frames: std::collections::HashSet,
+ pub last_update: std::time::Instant,
+}
+
+impl Default for TfTreeCache {
+ fn default() -> Self {
+ Self {
+ frames: HashMap::new(),
+ frame_last_seen: HashMap::new(),
+ static_frames: std::collections::HashSet::new(),
+ last_update: std::time::Instant::now(),
+ }
+ }
+}
+
+pub struct TfMonitor {
+ cache: Arc>,
+}
+
+impl TfMonitor {
+ pub fn new() -> Self {
+ Self {
+ cache: Arc::new(Mutex::new(TfTreeCache::default())),
+ }
+ }
+
+ pub fn get_cache(&self) -> Arc> {
+ self.cache.clone()
+ }
+
+ pub async fn start_monitoring(
+ &self,
+ node: &mut Node,
+ ) -> Result<(), Box> {
+ // Create QoS for /tf_static (static transforms - use TransientLocal)
+ let tf_static_qos = QosPolicyBuilder::new()
+ .history(policy::History::KeepLast { depth: 100 })
+ .reliability(Reliability::Reliable {
+ max_blocking_time: Duration::from_millis(100),
+ })
+ .durability(policy::Durability::TransientLocal)
+ .build();
+
+ // Create QoS for /tf (dynamic transforms - use Volatile to match publishers)
+ let tf_qos = QosPolicyBuilder::new()
+ .history(policy::History::KeepLast { depth: 100 })
+ .reliability(Reliability::Reliable {
+ max_blocking_time: Duration::from_millis(100),
+ })
+ .durability(policy::Durability::Volatile)
+ .build();
+
+ // Create /tf_static topic (static transforms)
+ let tf_static_topic = node.create_topic(
+ &Name::new("/", "tf_static")?,
+ MessageTypeName::new("tf2_msgs", "TFMessage"),
+ &tf_static_qos,
+ )?;
+
+ // Subscribe to /tf_static
+ let tf_static_subscription: Subscription =
+ node.create_subscription(&tf_static_topic, None)?;
+
+ // Create /tf topic (dynamic transforms)
+ let tf_topic = node.create_topic(
+ &Name::new("/", "tf")?,
+ MessageTypeName::new("tf2_msgs", "TFMessage"),
+ &tf_qos,
+ )?;
+
+ // Subscribe to /tf
+ let tf_subscription: Subscription = node.create_subscription(&tf_topic, None)?;
+
+ let cache = self.cache.clone();
+
+ // Spawn task to handle /tf_static messages
+ let cache_static = cache.clone();
+ tokio::spawn(async move {
+ debug!("Subscribed to /tf_static topic");
+ let mut stream = Box::pin(tf_static_subscription.async_stream());
+ let mut message_count = 0;
+ while let Some(result) = stream.next().await {
+ match result {
+ Ok((msg, _msg_info)) => {
+ message_count += 1;
+ debug!(
+ "Received /tf_static message #{} with {} transforms",
+ message_count,
+ msg.transforms.len()
+ );
+ Self::process_tf_message(&msg, &cache_static, true).await;
+ }
+ Err(e) => {
+ debug!("Error receiving /tf_static message: {:?}", e);
+ }
+ }
+ }
+ });
+
+ // Spawn task to handle /tf messages
+ let cache_dynamic = cache.clone();
+ tokio::spawn(async move {
+ debug!("Subscribed to /tf topic");
+ let mut stream = Box::pin(tf_subscription.async_stream());
+ let mut message_count = 0;
+ while let Some(result) = stream.next().await {
+ match result {
+ Ok((msg, _msg_info)) => {
+ message_count += 1;
+ // Only log first few messages, then every 1000 messages to avoid spam
+ if message_count <= 5 || message_count % 1000 == 0 {
+ let transform_names: Vec = msg
+ .transforms
+ .iter()
+ .map(|t| format!("{} -> {}", t.header.frame_id, t.child_frame_id))
+ .collect();
+ debug!(
+ "Received /tf message #{} with {} transforms: {:?}",
+ message_count,
+ msg.transforms.len(),
+ transform_names
+ );
+ }
+ Self::process_tf_message(&msg, &cache_dynamic, false).await;
+ }
+ Err(e) => {
+ debug!("Error receiving /tf message: {:?}", e);
+ }
+ }
+ }
+ });
+
+ // Spawn task to periodically log TF tree statistics and clean up stale frames
+ let cache_stats = cache.clone();
+ tokio::spawn(async move {
+ // Wait a bit before first check to allow some transforms to be collected
+ tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
+ let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(30));
+ loop {
+ interval.tick().await;
+ let cache_guard = cache_stats.lock().await;
+ let now = std::time::Instant::now();
+
+ // Clean up frames that haven't been seen for a while
+ // Static frames: use longer threshold (5 minutes) or don't clean at all
+ // Dynamic frames: use shorter threshold (30 seconds)
+ let dynamic_stale_threshold = std::time::Duration::from_secs(30);
+ let static_stale_threshold = std::time::Duration::from_secs(300); // 5 minutes
+
+ let frames_to_remove: Vec = cache_guard
+ .frame_last_seen
+ .iter()
+ .filter(|(frame_id, last_seen)| {
+ let age = now.duration_since(**last_seen);
+ let is_static = cache_guard.static_frames.contains(*frame_id);
+ if is_static {
+ // For static frames, use longer threshold
+ age > static_stale_threshold
+ } else {
+ // For dynamic frames, use shorter threshold
+ age > dynamic_stale_threshold
+ }
+ })
+ .map(|(frame_id, _)| frame_id.clone())
+ .collect();
+
+ for frame_id in &frames_to_remove {
+ debug!(
+ "TODO: remove stale TF frame: {}, but we keep it for now since the monitor algorithm has some issues",
+ frame_id
+ );
+
+ // // Remove the frame itself
+ // cache_guard.frames.remove(frame_id);
+ // cache_guard.frame_last_seen.remove(frame_id);
+ // cache_guard.static_frames.remove(frame_id);
+
+ // // Remove references to this frame from other frames' child_frames lists
+ // for frame in cache_guard.frames.values_mut() {
+ // frame.child_frames.retain(|child| child != frame_id);
+ // }
+
+ // // Remove this frame as parent from other frames
+ // for frame in cache_guard.frames.values_mut() {
+ // if frame.parent_frame.as_ref() == Some(frame_id) {
+ // frame.parent_frame = None;
+ // }
+ // }
+ }
+
+ let frame_count = cache_guard.frames.len();
+ let frame_names: Vec = cache_guard.frames.keys().cloned().collect();
+ let mut sorted_frames = frame_names.clone();
+ sorted_frames.sort();
+ debug!(
+ "TF tree statistics: {} frames total. Frames: {:?}",
+ frame_count, sorted_frames
+ );
+ }
+ });
+
+ Ok(())
+ }
+
+ async fn process_tf_message(msg: &TFMessage, cache: &Arc>, is_static: bool) {
+ let mut cache_guard = cache.lock().await;
+
+ let total_frames_before = cache_guard.frames.len();
+ let mut new_frames = Vec::new();
+
+ for transform_stamped in &msg.transforms {
+ let parent_frame = transform_stamped.header.frame_id.clone();
+ let child_frame = transform_stamped.child_frame_id.clone();
+
+ // Only log new frames to avoid spam
+ let is_new_frame = !cache_guard.frames.contains_key(&child_frame);
+ if is_new_frame {
+ new_frames.push(format!("{} -> {}", parent_frame, child_frame));
+ }
+
+ // Extract transform data
+ let tf_transform = TfTransform {
+ translation: [
+ transform_stamped.transform.translation.x,
+ transform_stamped.transform.translation.y,
+ transform_stamped.transform.translation.z,
+ ],
+ rotation: [
+ transform_stamped.transform.rotation.x,
+ transform_stamped.transform.rotation.y,
+ transform_stamped.transform.rotation.z,
+ transform_stamped.transform.rotation.w,
+ ],
+ };
+
+ // Update last seen time for both frames
+ let now = std::time::Instant::now();
+ cache_guard.frame_last_seen.insert(child_frame.clone(), now);
+ cache_guard
+ .frame_last_seen
+ .insert(parent_frame.clone(), now);
+
+ // Mark frames as static if they come from /tf_static
+ if is_static {
+ cache_guard.static_frames.insert(child_frame.clone());
+ cache_guard.static_frames.insert(parent_frame.clone());
+ }
+
+ // Update or create child frame
+ let child_frame_entry = cache_guard
+ .frames
+ .entry(child_frame.clone())
+ .or_insert_with(|| TfFrame {
+ frame_id: child_frame.clone(),
+ child_frames: vec![],
+ parent_frame: Some(parent_frame.clone()),
+ transform: None,
+ });
+ child_frame_entry.parent_frame = Some(parent_frame.clone());
+ child_frame_entry.transform = Some(tf_transform);
+
+ // Update or create parent frame
+ let parent_frame_entry = cache_guard
+ .frames
+ .entry(parent_frame.clone())
+ .or_insert_with(|| TfFrame {
+ frame_id: parent_frame.clone(),
+ child_frames: vec![],
+ parent_frame: None,
+ transform: None,
+ });
+ if !parent_frame_entry.child_frames.contains(&child_frame) {
+ parent_frame_entry.child_frames.push(child_frame);
+ }
+ }
+
+ let total_frames_after = cache_guard.frames.len();
+ if total_frames_after > total_frames_before {
+ debug!(
+ "TF cache updated: {} -> {} frames (added {} new frames: {:?})",
+ total_frames_before,
+ total_frames_after,
+ total_frames_after - total_frames_before,
+ new_frames
+ );
+ }
+ cache_guard.last_update = std::time::Instant::now();
+ }
+
+ pub async fn get_tree(&self) -> TfTreeResponse {
+ let cache = self.cache.lock().await;
+ let frames_map = cache.frames.clone();
+
+ // Find root frames (frames without parent)
+ let root_frames: Vec = frames_map
+ .values()
+ .filter(|f| f.parent_frame.is_none())
+ .map(|f| f.frame_id.clone())
+ .collect();
+
+ // If no root frames found, default to "world"
+ let root_frames = if root_frames.is_empty() {
+ vec!["world".to_string()]
+ } else {
+ root_frames
+ };
+
+ let frames: Vec = frames_map.into_values().collect();
+
+ TfTreeResponse {
+ frames,
+ root_frames,
+ }
+ }
+}
diff --git a/rust/robonix-core/src/perception/topic_monitor.rs b/rust/robonix-core/src/perception/topic_monitor.rs
new file mode 100644
index 0000000..4b731d6
--- /dev/null
+++ b/rust/robonix-core/src/perception/topic_monitor.rs
@@ -0,0 +1,171 @@
+// SPDX-License-Identifier: MulanPSL-2.0
+// Topic Monitor Module
+//
+// Monitors ROS2 topics and their types
+
+use log::debug;
+use ros2_client::Node;
+use serde::{Deserialize, Serialize};
+use std::collections::{HashMap, HashSet};
+use std::sync::Arc;
+use tokio::process::Command;
+use tokio::sync::Mutex;
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct TopicInfo {
+ pub name: String,
+ pub message_type: String,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct TopicsResponse {
+ pub topics: Vec,
+}
+
+pub struct TopicMonitor {
+ topics: Arc>>,
+}
+
+impl TopicMonitor {
+ pub fn new() -> Self {
+ Self {
+ topics: Arc::new(Mutex::new(HashMap::new())),
+ }
+ }
+
+ /// Get ROS2 setup.bash path, checking ROS_DISTRO environment variable first
+ fn get_ros2_setup_path() -> String {
+ let distro = std::env::var("ROS_DISTRO").unwrap_or_else(|_| "humble".to_string());
+ format!("/opt/ros/{}/setup.bash", distro)
+ }
+
+ pub async fn get_topics(&self) -> TopicsResponse {
+ // Refresh topics list on each call (on-demand discovery)
+ // This avoids maintaining a periodic async task
+ let _ = self.discover_topics().await; // Ignore errors, use cached data if discovery fails
+
+ let topics = self.topics.lock().await;
+ let mut topics_vec: Vec = topics.values().cloned().collect();
+ // Sort by topic name
+ topics_vec.sort_by(|a, b| a.name.cmp(&b.name));
+ TopicsResponse { topics: topics_vec }
+ }
+
+ /// Discover topics using ROS2 command line tools
+ /// Uses `ros2 topic list -t` to get all topics and their types in one command
+ pub async fn discover_topics(&self) -> Result<(), Box> {
+ // Get list of topics with types using ros2 topic list -t
+ // Source ROS2 environment first and add timeout to prevent hanging
+ let setup_path = Self::get_ros2_setup_path();
+ let output = tokio::time::timeout(
+ tokio::time::Duration::from_secs(3),
+ Command::new("bash")
+ .arg("-c")
+ .arg(format!(
+ "source {} && timeout 2 ros2 topic list -t",
+ setup_path
+ ))
+ .output(),
+ )
+ .await;
+
+ let output = match output {
+ Ok(Ok(output)) => output,
+ Ok(Err(e)) => {
+ return Err(format!("Failed to run ros2 topic list -t: {}", e).into());
+ }
+ Err(_) => {
+ debug!("ros2 topic list -t timed out");
+ return Ok(()); // Return empty result instead of error
+ }
+ };
+
+ if !output.status.success() {
+ let stderr = String::from_utf8_lossy(&output.stderr);
+ debug!("ros2 topic list -t failed: {}", stderr);
+ return Ok(()); // Return empty result instead of error
+ }
+
+ let topic_list = String::from_utf8_lossy(&output.stdout);
+
+ // Parse output: each line is "topic_name [message_type]"
+ // Example: "/amcl_pose [geometry_msgs/msg/PoseWithCovarianceStamped]"
+ let mut topics_map = self.topics.lock().await;
+ let mut current_topics_set = HashSet::new();
+
+ for line in topic_list.lines() {
+ let line = line.trim();
+ if line.is_empty() {
+ continue;
+ }
+
+ // Parse line: "topic_name [message_type1, message_type2, ...]" or just "topic_name"
+ // Example: "/clock [builtin_interfaces/msg/Time, rosgraph_msgs/msg/Clock]"
+ let parts: Vec<&str> = line.split_whitespace().collect();
+ if parts.is_empty() {
+ continue;
+ }
+
+ let topic_name = parts[0].to_string();
+ let message_type = if parts.len() >= 2 {
+ // Join all parts after topic name (handles multiple types in brackets)
+ // Example: "[builtin_interfaces/msg/Time," "rosgraph_msgs/msg/Clock]"
+ let types_str = parts[1..].join(" ");
+ // Remove brackets and trim
+ types_str
+ .trim_matches(|c| c == '[' || c == ']')
+ .trim()
+ .to_string()
+ } else {
+ "unknown".to_string()
+ };
+
+ current_topics_set.insert(topic_name.clone());
+
+ // Update or insert topic
+ topics_map.insert(
+ topic_name.clone(),
+ TopicInfo {
+ name: topic_name.clone(),
+ message_type: message_type.clone(),
+ },
+ );
+ }
+
+ // Remove topics that no longer exist
+ let topics_to_remove: Vec = topics_map
+ .keys()
+ .filter(|topic_name| !current_topics_set.contains(*topic_name))
+ .cloned()
+ .collect();
+
+ for topic_name in &topics_to_remove {
+ debug!("Removing topic {} (no longer exists)", topic_name);
+ topics_map.remove(topic_name);
+ }
+
+ debug!("Discovered {} topics", topics_map.len());
+ drop(topics_map);
+
+ Ok(())
+ }
+
+ /// Start monitoring topics by discovering them once
+ /// No periodic task - topics are discovered on-demand when get_topics() is called
+ pub async fn start_monitoring(
+ &self,
+ _node: &mut Node,
+ ) -> Result<(), Box> {
+ // Initial discovery
+ self.discover_topics().await?;
+ Ok(())
+ }
+}
+
+impl Clone for TopicMonitor {
+ fn clone(&self) -> Self {
+ Self {
+ topics: self.topics.clone(),
+ }
+ }
+}
diff --git a/rust/robonix-core/src/primitive/mod.rs b/rust/robonix-core/src/primitive/mod.rs
index 01a271c..41cc8ab 100644
--- a/rust/robonix-core/src/primitive/mod.rs
+++ b/rust/robonix-core/src/primitive/mod.rs
@@ -17,11 +17,12 @@ use tokio::sync::RwLock;
#[derive(Debug, Clone)]
struct PrimitiveEntry {
name: String,
- input_schema: serde_json::Value,
- output_schema: serde_json::Value,
- metadata: serde_json::Value,
+ input_schema: String, // JSON string: stored as string internally
+ output_schema: String, // JSON string: stored as string internally
+ metadata: String, // JSON string: stored as string internally
provider: String,
version: String,
+ node_id: String,
}
/// Primitive Registry - Manages primitive registration and querying
@@ -46,7 +47,7 @@ impl PrimitiveRegistry {
&self,
req: RegisterPrimitiveRequest,
) -> RegisterPrimitiveResponse {
- // Parse JSON strings
+ // Validate JSON format and parse for spec validation
let input_schema: serde_json::Value = match serde_json::from_str(&req.input_schema) {
Ok(v) => v,
Err(e) => {
@@ -67,16 +68,14 @@ impl PrimitiveRegistry {
return RegisterPrimitiveResponse { ok: false };
}
};
- let metadata: serde_json::Value = match serde_json::from_str(&req.metadata) {
- Ok(v) => v,
- Err(e) => {
- warn!(
- "failed to parse metadata json: primitive_name={}, provider={}, error={}",
- req.name, req.provider, e
- );
- return RegisterPrimitiveResponse { ok: false };
- }
- };
+ // Validate metadata JSON format
+ if serde_json::from_str::(&req.metadata).is_err() {
+ warn!(
+ "failed to parse metadata json: primitive_name={}, provider={}",
+ req.name, req.provider
+ );
+ return RegisterPrimitiveResponse { ok: false };
+ }
// Validate against spec
match self
@@ -96,15 +95,17 @@ impl PrimitiveRegistry {
}
// Key includes name, provider, and version to distinguish different implementations
- let key = format!("{}::{}::{}", req.name, req.provider, req.version);
+ let key = format!("{}${}${}", req.name, req.provider, req.version);
+ // Store as JSON strings internally
let entry = PrimitiveEntry {
name: req.name.clone(),
- input_schema,
- output_schema,
- metadata,
+ input_schema: req.input_schema.clone(),
+ output_schema: req.output_schema.clone(),
+ metadata: req.metadata.clone(),
provider: req.provider.clone(),
version: req.version.clone(),
+ node_id: req.node_id.clone(),
};
let mut primitives = self.primitives.write().await;
@@ -134,7 +135,13 @@ impl PrimitiveRegistry {
Ok(v) => v,
Err(_) => continue, // Skip if filter is invalid JSON
};
- if !self.matches_filter(&entry.metadata, &filter_value) {
+ // Parse metadata for filtering
+ let metadata_value: serde_json::Value = match serde_json::from_str(&entry.metadata)
+ {
+ Ok(v) => v,
+ Err(_) => continue, // Skip if metadata is invalid JSON
+ };
+ if !self.matches_filter(&metadata_value, &filter_value) {
continue;
}
}
@@ -145,12 +152,33 @@ impl PrimitiveRegistry {
input_schema: entry.input_schema.clone(),
output_schema: entry.output_schema.clone(),
metadata: entry.metadata.clone(),
+ node_id: entry.node_id.clone(),
});
}
QueryPrimitiveResponse { instances }
}
+ /// Get all registered primitives (for web UI)
+ pub async fn get_all_primitives(&self) -> Vec<(String, PrimitiveInstance)> {
+ let primitives = self.primitives.read().await;
+ let mut result = Vec::new();
+ for (key, entry) in primitives.iter() {
+ result.push((
+ key.clone(),
+ PrimitiveInstance {
+ provider: entry.provider.clone(),
+ version: entry.version.clone(),
+ input_schema: entry.input_schema.clone(),
+ output_schema: entry.output_schema.clone(),
+ metadata: entry.metadata.clone(),
+ node_id: entry.node_id.clone(),
+ },
+ ));
+ }
+ result
+ }
+
/// Check if metadata matches filter
/// Supports simple equality and comparison operators (>=, <=, >, <) for numeric values
fn matches_filter(&self, metadata: &serde_json::Value, filter: &serde_json::Value) -> bool {
diff --git a/rust/robonix-core/src/ros_idl/get_listening_ips.rs b/rust/robonix-core/src/ros_idl/get_listening_ips.rs
new file mode 100644
index 0000000..1e909f3
--- /dev/null
+++ b/rust/robonix-core/src/ros_idl/get_listening_ips.rs
@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: MulanPSL-2.0
+// GetListeningIps ROS IDL
+//
+// Service for core to report all IP addresses it is listening on.
+// Used by CLI daemon to discover core's network addresses via ROS2.
+
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct GetListeningIpsRequest {
+ /// Unused; empty request. Kept for ROS2 service request shape.
+ #[serde(default)]
+ pub _dummy: u8,
+}
+
+impl ros2_client::Message for GetListeningIpsRequest {}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct GetListeningIpsResponse {
+ /// JSON array of IP address strings, e.g. ["192.168.1.10", "10.0.0.1"]
+ pub ips_json: String,
+}
+
+impl ros2_client::Message for GetListeningIpsResponse {}
diff --git a/rust/robonix-core/src/ros_idl/mod.rs b/rust/robonix-core/src/ros_idl/mod.rs
index 64d5b61..d0e97b7 100644
--- a/rust/robonix-core/src/ros_idl/mod.rs
+++ b/rust/robonix-core/src/ros_idl/mod.rs
@@ -4,6 +4,7 @@
// This module contains all ROS IDL message type definitions used for communication.
// These types implement ros2_client::Message and are used in ROS2 service interfaces.
+pub mod get_listening_ips;
pub mod object; // robonix_sdk Object message types
pub mod primitive;
pub mod service;
diff --git a/rust/robonix-core/src/ros_idl/primitive.rs b/rust/robonix-core/src/ros_idl/primitive.rs
index d340b0f..9bb8e25 100644
--- a/rust/robonix-core/src/ros_idl/primitive.rs
+++ b/rust/robonix-core/src/ros_idl/primitive.rs
@@ -12,6 +12,9 @@ pub struct RegisterPrimitiveRequest {
pub metadata: String, // JSON string: metadata for instance filtering
pub provider: String, // Primitive provider identifier
pub version: String, // Implementation version (e.g., "1.0.0", "1.0.0-alpha")
+ /// Node (CLI client) that registered this capability; e.g. hostname. Empty for backward compat.
+ #[serde(default)]
+ pub node_id: String,
}
impl ros2_client::Message for RegisterPrimitiveRequest {}
@@ -36,9 +39,12 @@ impl ros2_client::Message for QueryPrimitiveRequest {}
pub struct PrimitiveInstance {
pub provider: String,
pub version: String,
- pub input_schema: serde_json::Value,
- pub output_schema: serde_json::Value,
- pub metadata: serde_json::Value,
+ pub input_schema: String, // JSON string: {"argname0":"/topic0", ...}
+ pub output_schema: String, // JSON string: {"argname1":"/topic1", ...}
+ pub metadata: String, // JSON string: metadata for instance filtering
+ /// Node that registered this capability. Empty if unknown.
+ #[serde(default)]
+ pub node_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
diff --git a/rust/robonix-core/src/ros_idl/service_registry.rs b/rust/robonix-core/src/ros_idl/service_registry.rs
index 2212ce6..a3c6cf8 100644
--- a/rust/robonix-core/src/ros_idl/service_registry.rs
+++ b/rust/robonix-core/src/ros_idl/service_registry.rs
@@ -15,6 +15,9 @@ pub struct RegisterServiceRequest {
pub metadata: String, // JSON string: metadata for instance filtering
pub provider: String, // Service provider identifier
pub version: String, // Implementation version (e.g., "1.0.0", "1.0.0-alpha")
+ /// Node (CLI client) that registered this capability. Empty for backward compat.
+ #[serde(default)]
+ pub node_id: String,
}
impl ros2_client::Message for RegisterServiceRequest {}
@@ -40,7 +43,10 @@ pub struct ServiceInstance {
pub provider: String,
pub version: String,
pub entry: String,
- pub metadata: serde_json::Value,
+ pub metadata: String, // JSON string: metadata for instance filtering
+ /// Node that registered this capability. Empty if unknown.
+ #[serde(default)]
+ pub node_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
diff --git a/rust/robonix-core/src/ros_idl/skill.rs b/rust/robonix-core/src/ros_idl/skill.rs
index be072c7..7b14657 100644
--- a/rust/robonix-core/src/ros_idl/skill.rs
+++ b/rust/robonix-core/src/ros_idl/skill.rs
@@ -18,6 +18,9 @@ pub struct RegisterSkillRequest {
pub metadata: String, // JSON string: metadata for instance filtering
pub provider: String, // Skill provider identifier
pub version: String, // Skill version
+ /// Node (CLI client) that registered this capability. Empty for backward compat.
+ #[serde(default)]
+ pub node_id: String,
}
impl ros2_client::Message for RegisterSkillRequest {}
@@ -42,17 +45,21 @@ impl ros2_client::Message for QuerySkillRequest {}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SkillInstance {
pub skill_id: String,
+ pub name: String, // Standard skill name (e.g., "skl::wandering")
pub provider: String,
pub version: String,
pub r#type: String, // Skill type: "basic" | "rtdl"
pub start_topic: String,
pub status_topic: String,
- pub entry: String, // Basic skill entry (if type="basic")
- pub skill_dir: String, // Skill directory path (if type="rtdl")
- pub main_rtdl: String, // Main RTDL file name (if type="rtdl")
- pub start_args: serde_json::Value,
- pub status: serde_json::Value,
- pub metadata: serde_json::Value,
+ pub entry: String, // Basic skill entry (if type="basic")
+ pub skill_dir: String, // Skill directory path (if type="rtdl")
+ pub main_rtdl: String, // Main RTDL file name (if type="rtdl")
+ pub start_args: String, // JSON string: input parameter schema
+ pub status: String, // JSON string: status feedback schema
+ pub metadata: String, // JSON string: metadata for instance filtering
+ /// Node that registered this capability. Empty if unknown.
+ #[serde(default)]
+ pub node_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -61,3 +68,11 @@ pub struct QuerySkillResponse {
}
impl ros2_client::Message for QuerySkillResponse {}
+
+// https://docs.ros.org/en/melodic/api/std_msgs/html/msg/String.html
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct StdString {
+ pub data: String,
+}
+
+impl ros2_client::Message for StdString {}
diff --git a/rust/robonix-core/src/ros_idl/task.rs b/rust/robonix-core/src/ros_idl/task.rs
index 65e7e45..9b56843 100644
--- a/rust/robonix-core/src/ros_idl/task.rs
+++ b/rust/robonix-core/src/ros_idl/task.rs
@@ -33,3 +33,18 @@ pub struct TaskDataResponse {
}
impl ros2_client::Message for TaskDataResponse {}
+
+/// Cancel task request (robonix spec)
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct CancelTaskRequest {
+ pub task_id: String,
+}
+
+impl ros2_client::Message for CancelTaskRequest {}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct CancelTaskResponse {
+ pub success: bool,
+}
+
+impl ros2_client::Message for CancelTaskResponse {}
diff --git a/rust/robonix-core/src/server.rs b/rust/robonix-core/src/server.rs
index 162aadc..4787584 100644
--- a/rust/robonix-core/src/server.rs
+++ b/rust/robonix-core/src/server.rs
@@ -4,6 +4,7 @@
// Handles ROS2 service server creation and request handling
use crate::core::RobonixCore;
+use crate::ros_idl::get_listening_ips::{GetListeningIpsRequest, GetListeningIpsResponse};
use crate::ros_idl::primitive::{
QueryPrimitiveRequest, QueryPrimitiveResponse, RegisterPrimitiveRequest,
RegisterPrimitiveResponse,
@@ -15,11 +16,12 @@ use crate::ros_idl::skill::{
QuerySkillRequest, QuerySkillResponse, RegisterSkillRequest, RegisterSkillResponse,
};
use crate::ros_idl::task::{
- SubmitTaskRequest, SubmitTaskResponse, TaskDataRequest, TaskDataResponse,
+ CancelTaskRequest, CancelTaskResponse, SubmitTaskRequest, SubmitTaskResponse, TaskDataRequest,
+ TaskDataResponse,
};
use crate::ros_idl::test::{PingPongRequest, PingPongResponse};
use futures_util::stream::StreamExt;
-use log::{debug, error, info};
+use log::{error, info, warn};
use ros2_client::{
AService, Name, Node, Server, ServiceMapping, ServiceTypeName,
rustdds::{
@@ -29,9 +31,114 @@ use ros2_client::{
};
use std::sync::Arc;
+/// Return all IP addresses this host is listening on (for daemon to discover core).
+fn get_listening_ips() -> Vec {
+ #[cfg(target_os = "linux")]
+ {
+ match std::process::Command::new("hostname").arg("-I").output() {
+ Ok(output) if output.status.success() => {
+ let s = String::from_utf8_lossy(&output.stdout);
+ let ips: Vec = s
+ .split_whitespace()
+ .map(|s| s.trim().to_string())
+ .filter(|s| !s.is_empty())
+ .collect();
+ if !ips.is_empty() {
+ return ips;
+ }
+ }
+ _ => {}
+ }
+ }
+ #[cfg(not(target_os = "linux"))]
+ {
+ let _ = std::process::Command::new("hostname");
+ }
+ // Fallback: try to get one IP via UDP socket
+ if let Ok(socket) = std::net::UdpSocket::bind("0.0.0.0:0") {
+ if let Ok(()) = socket.connect("8.8.8.8:80") {
+ if let Ok(addr) = socket.local_addr() {
+ return vec![addr.ip().to_string()];
+ }
+ }
+ }
+ Vec::new()
+}
+
+/// Set thread to real-time priority with SCHED_FIFO policy
+/// This ensures the thread has the highest priority and won't be preempted by other user threads
+fn set_thread_realtime_priority() {
+ #[cfg(target_os = "linux")]
+ {
+ use std::mem;
+ unsafe {
+ let mut param: libc::sched_param = mem::zeroed();
+ // Set priority to maximum (99 is the highest for SCHED_FIFO)
+ // Note: This requires CAP_SYS_NICE capability or running as root
+ param.sched_priority = 99;
+
+ let result =
+ libc::pthread_setschedparam(libc::pthread_self(), libc::SCHED_FIFO, ¶m);
+
+ if result == 0 {
+ info!("Thread set to SCHED_FIFO with priority 99 (highest priority)");
+ } else {
+ let errno = *libc::__errno_location();
+ warn!(
+ "Failed to set thread to real-time priority (errno: {}). \
+ This may require root privileges or CAP_SYS_NICE capability. \
+ Thread will run with normal priority.",
+ errno
+ );
+ }
+ }
+ }
+
+ #[cfg(not(target_os = "linux"))]
+ {
+ warn!("Real-time priority setting is only supported on Linux");
+ }
+}
+
+/// Spawn a core service API handler on a dedicated high-priority thread
+/// Each core ROS service API gets its own thread with real-time priority
+fn spawn_core_api_thread(name: &str, f: F)
+where
+ F: FnOnce() -> Fut + Send + 'static,
+ Fut: std::future::Future