From c922f5b7632d1681f1e4151fac0343dbaaa59999 Mon Sep 17 00:00:00 2001 From: Liangwei XU Date: Tue, 20 Jan 2026 11:19:10 +0800 Subject: [PATCH 01/10] feat(mock_plugin): Implement mock plugin with C ABI interface and E2E tests --- Makefile | 61 ++- apps/axon_recorder/test/CMakeLists.txt | 37 +- apps/axon_recorder/test/README.md | 3 +- apps/axon_recorder/test/e2e/CMakeLists.txt | 24 - apps/axon_recorder/test/e2e/README.md | 255 ---------- apps/axon_recorder/test/e2e/run_e2e_tests.sh | 15 +- apps/plugin_example/plugin_loader_test.cpp | 19 +- .../run_test_with_ros2_tools.sh | 104 ++++ apps/plugin_example/test_with_ros2_tools.cpp | 183 +++++++ .../mock/src/mock_plugin/CMakeLists.txt | 96 ++++ .../src/mock_plugin/include/mock_plugin.hpp | 83 ++++ .../mock/src/mock_plugin/src/mock_plugin.cpp | 190 +++++++ .../mock_plugin/src/mock_plugin_export.cpp | 215 ++++++++ .../mock_plugin/test/test_mock_plugin_e2e.cpp | 171 +++++++ .../test/test_mock_plugin_load.cpp | 159 ++++++ middlewares/mock/test_e2e_with_mock.sh | 466 ++++++++++++++++++ middlewares/mock/test_full_workflow.sh | 66 +++ 17 files changed, 1825 insertions(+), 322 deletions(-) delete mode 100644 apps/axon_recorder/test/e2e/CMakeLists.txt delete mode 100644 apps/axon_recorder/test/e2e/README.md create mode 100644 apps/plugin_example/run_test_with_ros2_tools.sh create mode 100644 apps/plugin_example/test_with_ros2_tools.cpp create mode 100644 middlewares/mock/src/mock_plugin/CMakeLists.txt create mode 100644 middlewares/mock/src/mock_plugin/include/mock_plugin.hpp create mode 100644 middlewares/mock/src/mock_plugin/src/mock_plugin.cpp create mode 100644 middlewares/mock/src/mock_plugin/src/mock_plugin_export.cpp create mode 100644 middlewares/mock/src/mock_plugin/test/test_mock_plugin_e2e.cpp create mode 100644 middlewares/mock/src/mock_plugin/test/test_mock_plugin_load.cpp create mode 100644 middlewares/mock/test_e2e_with_mock.sh create mode 100644 middlewares/mock/test_full_workflow.sh diff --git a/Makefile b/Makefile index a08a767..e675219 100644 --- a/Makefile +++ b/Makefile @@ -97,6 +97,13 @@ help: @printf "%s\n" " $(BLUE)make app-axon-recorder$(NC) - Build axon_recorder plugin loader" @printf "%s\n" " $(BLUE)make app-plugin-example$(NC) - Build plugin example" @echo "" + @printf "%s\n" "$(YELLOW)Mock Middleware (Testing):$(NC)" + @printf "%s\n" " $(BLUE)make build-mock$(NC) - Build mock middleware plugin" + @printf "%s\n" " $(BLUE)make test-mock-e2e$(NC) - Run mock plugin E2E test (standalone)" + @printf "%s\n" " $(BLUE)make test-mock-load$(NC) - Run mock plugin load test" + @printf "%s\n" " $(BLUE)make test-mock-integration$(NC) - Run mock middleware integration E2E test" + @printf "%s\n" " $(BLUE)make test-mock-all$(NC) - Run all mock middleware tests" + @echo "" @printf "%s\n" "$(YELLOW)ROS Middlewares:$(NC)" @printf "%s\n" " $(BLUE)make build$(NC) - Build (auto-detects ROS1/ROS2)" @printf "%s\n" " $(BLUE)make build-ros1$(NC) - Build ROS1 (Noetic)" @@ -220,10 +227,17 @@ test-mcap: build-mcap test-uploader: build-core @printf "%s\n" "$(YELLOW)Running axon_uploader tests...$(NC)" @if [ -d "$(BUILD_DIR)/axon_uploader" ]; then \ +<<<<<<< HEAD cd $(BUILD_DIR)/axon_uploader && ctest --output-on-failure && \ printf "%s\n" "$(GREEN)✓ axon_uploader tests passed$(NC)"; \ else \ printf "%s\n" "$(YELLOW)⚠ axon_uploader not built (requires AWS SDK, enable with -DAXON_BUILD_UPLOADER=ON)$(NC)"; \ +======= + cd $(BUILD_DIR)/axon_uploader && ctest --output-on-failure; \ + printf "%s\n" "$(GREEN)✓ axon_uploader tests passed$(NC)"; \ + else \ + printf "%s\n" "$(YELLOW)⚠ axon_uploader not built, skipping tests$(NC)"; \ +>>>>>>> 5bf3c1b (feat(mock_plugin): Implement mock plugin with C ABI interface and E2E tests) fi # Test axon_logging @@ -416,6 +430,46 @@ app-axon-recorder: build-core app-plugin-example: build-core @printf "%s\n" "$(GREEN)✓ plugin example built$(NC)" +# ============================================================================= +# Mock Middleware Targets +# ============================================================================= + +# Build mock middleware +build-mock: + @printf "%s\n" "$(YELLOW)Building mock middleware...$(NC)" + @mkdir -p middlewares/mock/src/mock_plugin/build + @cd middlewares/mock/src/mock_plugin/build && \ + cmake .. \ + -DCMAKE_BUILD_TYPE=$(BUILD_TYPE) \ + -DCMAKE_INSTALL_PREFIX=$(PROJECT_ROOT)/middlewares/mock/install && \ + cmake --build . -j$(NPROC) + @printf "%s\n" "$(GREEN)✓ Mock middleware built$(NC)" + +# Test mock plugin E2E +test-mock-e2e: build-mock + @printf "%s\n" "$(YELLOW)Running mock plugin E2E test...$(NC)" + @cd middlewares/mock/src/mock_plugin/build && ./test_mock_plugin_e2e + @printf "%s\n" "$(GREEN)✓ Mock plugin E2E test passed$(NC)" + +# Test mock plugin load +test-mock-load: build-mock + @printf "%s\n" "$(YELLOW)Running mock plugin load test...$(NC)" + @cd middlewares/mock/src/mock_plugin/build && \ + ./test_mock_plugin_load ./libmock_plugin.so + @printf "%s\n" "$(GREEN)✓ Mock plugin load test passed$(NC)" + +# Test mock middleware integration with axon_recorder +test-mock-integration: build-mock build-core + @printf "%s\n" "$(YELLOW)Running mock middleware integration E2E test...$(NC)" + @cd middlewares/mock && ./test_e2e_with_mock.sh + @printf "%s\n" "$(GREEN)✓ Mock middleware integration test passed$(NC)" + +# Run all mock middleware tests +test-mock-all: build-mock + @printf "%s\n" "$(YELLOW)Running all mock middleware tests...$(NC)" + @cd middlewares/mock && ./test_full_workflow.sh + @printf "%s\n" "$(GREEN)✓ All mock middleware tests passed$(NC)" + # ============================================================================= # ROS Middleware Targets # ============================================================================= @@ -483,6 +537,12 @@ test: test-core clean: @printf "%s\n" "$(YELLOW)Cleaning build artifacts...$(NC)" @rm -rf $(BUILD_DIR) $(COVERAGE_DIR) +<<<<<<< HEAD +======= + @cd middlewares/ros2 && rm -rf build install log 2>/dev/null || true + @cd middlewares/ros1 && catkin clean --yes 2>/dev/null || true + @rm -rf middlewares/mock/src/mock_plugin/build middlewares/mock/install 2>/dev/null || true +>>>>>>> 5bf3c1b (feat(mock_plugin): Implement mock plugin with C ABI interface and E2E tests) @printf "%s\n" "$(GREEN)✓ All build artifacts cleaned$(NC)" # Install target @@ -1174,4 +1234,3 @@ format-ci: printf "%s\n" "$(YELLOW)Or format files individually: $(CLANG_FORMAT) -i $(NC)" && \ exit 1) @printf "%s\n" "$(GREEN)✓ Code formatting check passed$(NC)" - diff --git a/apps/axon_recorder/test/CMakeLists.txt b/apps/axon_recorder/test/CMakeLists.txt index 461125d..62bb617 100644 --- a/apps/axon_recorder/test/CMakeLists.txt +++ b/apps/axon_recorder/test/CMakeLists.txt @@ -328,39 +328,14 @@ endif() # ============================================================================= # E2E Tests # ============================================================================= -# Build mock middleware plugin for E2E testing -option(AXON_BUILD_E2E_TESTS "Build E2E test support (mock plugin)" ON) +# E2E tests now use the unified mock middleware from middlewares/mock/ +# Build with: make build-mock from project root +option(AXON_BUILD_E2E_TESTS "Build E2E test support" ON) if(AXON_BUILD_E2E_TESTS) - message(STATUS "Configuring E2E test support...") - - set(E2E_TEST_DIR ${CMAKE_CURRENT_SOURCE_DIR}/e2e) - - # Build mock middleware plugin - add_library(axon_mock MODULE - ${E2E_TEST_DIR}/mock_middleware.cpp - ) - - target_include_directories(axon_mock PRIVATE - ${CMAKE_SOURCE_DIR}/include - ) - - target_compile_features(axon_mock PRIVATE cxx_std_17) - - # Set output properties - set_target_properties(axon_mock PROPERTIES - PREFIX "" - OUTPUT_NAME "axon_mock" - LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/middlewares - ) - - # Installation - install(TARGETS axon_mock - LIBRARY DESTINATION lib/middlewares - ) - - message(STATUS "E2E test support configured successfully") - message(STATUS " Mock plugin: ${CMAKE_BINARY_DIR}/middlewares/libaxon_mock.so") + message(STATUS "E2E tests configured to use mock middleware from middlewares/mock/") + message(STATUS " Build mock plugin with: make build-mock") + message(STATUS " Mock plugin location: middlewares/mock/src/mock_plugin/build/libmock_plugin.so") endif() # ============================================================================= diff --git a/apps/axon_recorder/test/README.md b/apps/axon_recorder/test/README.md index 36c5581..53fdac6 100644 --- a/apps/axon_recorder/test/README.md +++ b/apps/axon_recorder/test/README.md @@ -24,8 +24,7 @@ test/ └── e2e/ # End-to-end tests (full workflow) ├── run_e2e_tests.sh # Main E2E test script ├── run_docker_e2e.sh # Docker-based E2E tests - ├── mock_middleware.cpp # Mock middleware plugin - ├── CMakeLists.txt # Mock plugin build config + ├── CMakeLists.txt # E2E test config └── README.md # E2E test documentation ``` diff --git a/apps/axon_recorder/test/e2e/CMakeLists.txt b/apps/axon_recorder/test/e2e/CMakeLists.txt deleted file mode 100644 index c7f6287..0000000 --- a/apps/axon_recorder/test/e2e/CMakeLists.txt +++ /dev/null @@ -1,24 +0,0 @@ -cmake_minimum_required(VERSION 3.12) - -project(axon_mock CXX) - -# Build mock middleware plugin for E2E testing -# Note: No external ABI header dependency - plugin defines its own ABI structures -add_library(axon_mock MODULE - mock_middleware.cpp -) - -target_compile_features(axon_mock PRIVATE cxx_std_17) - -# Set output properties -set_target_properties(axon_mock PROPERTIES - PREFIX "" - OUTPUT_NAME "axon_mock" - LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} -) - -# Installation (optional) -install(TARGETS axon_mock - LIBRARY DESTINATION lib/middlewares -) - diff --git a/apps/axon_recorder/test/e2e/README.md b/apps/axon_recorder/test/e2e/README.md deleted file mode 100644 index 35612b1..0000000 --- a/apps/axon_recorder/test/e2e/README.md +++ /dev/null @@ -1,255 +0,0 @@ -# E2E Tests for Axon Recorder - -This directory contains end-to-end tests for the axon_recorder application. - -## Overview - -The E2E tests verify the complete recording workflow from start to finish: - -1. **Process Management**: Starting and stopping the recorder -2. **HTTP API**: Testing all REST endpoints -3. **Recording Workflow**: Cache config → Start → Pause → Resume → Stop -4. **File Generation**: MCAP file and sidecar JSON verification -5. **Metadata Validation**: Checking task metadata in output files - -## Test Structure - -``` -e2e/ -├── run_e2e_tests.sh # Main E2E test script -├── run_docker_e2e.sh # Docker-based E2E test runner -├── README.md # This file -└── test_data/ # Generated during test execution - ├── recordings/ # MCAP files - ├── recorder.log # Recorder output - └── stats.json # Statistics -``` - -## Prerequisites - -### Local Testing - -```bash -# Build the project -cd /path/to/Axon -make build - -# Install dependencies (if not already installed) -sudo apt-get install curl python3 -``` - -### Docker Testing - -```bash -# Install Docker -sudo apt-get install docker.io -``` - -## Running Tests - -### Local Environment - -```bash -# Run all E2E tests -cd apps/axon_recorder/test/e2e -./run_e2e_tests.sh -``` - -### Docker Environment - -```bash -# Run in ROS2 Humble environment -cd apps/axon_recorder/test/e2e -./run_docker_e2e.sh - -# Specify ROS version -ROS_VERSION=rolling ./run_docker_e2e.sh -``` - -## Test Coverage - -### HTTP API Tests - -| Test | Endpoint | Description | -|------|----------|-------------| -| `test_health_check` | `GET /health` | Verify recorder is running | -| `test_cache_config` | `POST /api/v1/config/cache` | Cache task configuration | -| `test_start_recording` | `POST /api/v1/recording/start` | Start recording | -| `test_recording_status` | `GET /api/v1/recording/status` | Check recording state | -| `test_pause_recording` | `POST /api/v1/recording/pause` | Pause recording | -| `test_resume_recording` | `POST /api/v1/recording/resume` | Resume recording | -| `test_stop_recording` | `POST /api/v1/recording/stop` | Stop recording | - -### File Verification Tests - -| Test | Description | -|------|-------------| -| `verify_mcap_file` | Check MCAP file exists and is not empty | -| `verify_sidecar_file` | Validate sidecar JSON and required fields | - -### Sidecar Validation - -The sidecar JSON file is validated for: - -- **Valid JSON**: Parses correctly -- **Required fields**: `version`, `task_id`, `device_id`, `scene`, `recording_start_time`, `recording_end_time`, `checksum` -- **Task metadata**: Matches the cached configuration - -## Test Flow - -``` -1. Setup - ├── Create test data directory - ├── Check recorder binary exists - └── Generate test configuration - -2. Start Recorder - ├── Launch axon_recorder process - ├── Wait for startup - └── Verify process is running - -3. HTTP API Tests - ├── Health check - ├── Cache configuration - ├── Start recording - ├── Check status (RECORDING) - ├── Pause recording - ├── Check status (PAUSED) - ├── Resume recording - └── Stop recording - -4. File Verification - ├── Find MCAP file - ├── Check file size > 0 - ├── Find sidecar JSON - ├── Validate JSON syntax - └── Check required fields - -5. Cleanup - ├── Stop recorder process - └── Remove test data -``` - -## Configuration - -### Test Configuration - -Tests use the following defaults: - -| Variable | Default | Description | -|----------|---------|-------------| -| `HTTP_PORT` | 8080 | HTTP server port | -| `TEST_TASK_ID` | `e2e_test_` | Unique task ID | -| `TEST_DEVICE_ID` | `test_robot_01` | Device identifier | -| `TEST_SCENE` | `e2e_test_scene` | Scene name | - -### Custom Configuration - -You can modify test parameters by editing the configuration section in `run_e2e_tests.sh`: - -```bash -# Test configuration -HTTP_PORT=8080 -TEST_TASK_ID="e2e_test_$(date +%s)" -TEST_DEVICE_ID="test_robot_01" -TEST_SCENE="e2e_test_scene" -``` - -## Troubleshooting - -### Recorder Fails to Start - -Check the recorder log: -```bash -cat test_data/recorder.log -``` - -### HTTP Requests Fail - -Verify the recorder is running: -```bash -curl http://localhost:8080/health -``` - -### MCAP File Not Created - -1. Check the dataset path in test config -2. Verify disk space is available -3. Review recorder logs for errors - -### Sidecar File Missing - -1. Verify task config was cached -2. Check that recording was stopped properly -3. Look for errors in metadata injection - -## Adding New Tests - -To add a new E2E test: - -1. Create a test function in `run_e2e_tests.sh`: - ```bash - test_my_new_feature() { - log_info "Testing my new feature..." - # Your test code here - if [[ condition ]]; then - log_info "Test passed" - return 0 - else - log_error "Test failed" - return 1 - fi - } - ``` - -2. Add the test to the `tests` array: - ```bash - local tests=( - "test_health_check" - "test_my_new_feature" # Add here - # ... other tests - ) - ``` - -3. Run the tests and verify: - ```bash - ./run_e2e_tests.sh - ``` - -## CI/CD Integration - -### GitHub Actions Example - -```yaml -name: E2E Tests - -on: [push, pull_request] - -jobs: - e2e: - runs-on: ubuntu-latest - strategy: - matrix: - ros_version: [humble, jazzy, rolling] - steps: - - uses: actions/checkout@v3 - - name: Run E2E tests - run: | - cd apps/axon_recorder/test/e2e - ROS_VERSION=${{ matrix.ros_version }} ./run_docker_e2e.sh -``` - -## Cleanup - -To manually clean up test artifacts: - -```bash -# Stop any running recorder -pkill -f axon_recorder - -# Remove test data -rm -rf apps/axon_recorder/test/e2e/test_data - -# Remove Docker artifacts -docker system prune -f -``` diff --git a/apps/axon_recorder/test/e2e/run_e2e_tests.sh b/apps/axon_recorder/test/e2e/run_e2e_tests.sh index 271ef8d..78e0867 100755 --- a/apps/axon_recorder/test/e2e/run_e2e_tests.sh +++ b/apps/axon_recorder/test/e2e/run_e2e_tests.sh @@ -32,8 +32,16 @@ else RECORDER_BIN="${BUILD_DIR}/axon_recorder/axon_recorder" fi -# Mock plugin path -MOCK_PLUGIN="${BUILD_DIR}/middlewares/axon_mock.so" +# Mock plugin path - use the newly created mock middleware +MOCK_PLUGIN="${PROJECT_ROOT}/middlewares/mock/src/mock_plugin/build/libmock_plugin.so" +if [[ ! -f "${MOCK_PLUGIN}" ]]; then + # Fallback to build directory + MOCK_PLUGIN="${BUILD_DIR}/middlewares/mock/src/mock_plugin/build/libmock_plugin.so" +fi +if [[ ! -f "${MOCK_PLUGIN}" ]]; then + # Another fallback for backward compatibility + MOCK_PLUGIN="${BUILD_DIR}/middlewares/axon_mock.so" +fi if [[ ! -f "${MOCK_PLUGIN}" ]]; then MOCK_PLUGIN="${PROJECT_ROOT}/apps/axon_recorder/build/axon_mock.so" fi @@ -113,7 +121,8 @@ setup() { if [[ ! -f "${MOCK_PLUGIN}" ]]; then log_warn "Mock plugin not found at ${MOCK_PLUGIN}" log_info "E2E tests will attempt to run without mock middleware" - log_info "Build with: cd apps/axon_recorder/test/e2e && cmake . && make" + log_info "Build mock middleware with: make build-mock" + log_info "Or from project root: cd middlewares/mock/src/mock_plugin/build && cmake .. && make" MOCK_PLUGIN="" fi diff --git a/apps/plugin_example/plugin_loader_test.cpp b/apps/plugin_example/plugin_loader_test.cpp index 8430be5..7d3a306 100644 --- a/apps/plugin_example/plugin_loader_test.cpp +++ b/apps/plugin_example/plugin_loader_test.cpp @@ -248,14 +248,21 @@ int main(int argc, char* argv[]) { std::signal(SIGTERM, signal_handler); // Parse command line arguments - std::string plugin_path = - "/home/xlw/src/Axon/middlewares/ros2/install/axon_ros2_plugin/lib/axon/plugins/" - "libaxon_ros2_plugin.so"; - int test_duration_seconds = 10; - - if (argc > 1) { + // Plugin path must be provided via command line argument or environment variable + std::string plugin_path; + const char* env_path = std::getenv("AXON_ROS2_PLUGIN_PATH"); + if (env_path) { + plugin_path = env_path; + } else if (argc > 1) { plugin_path = argv[1]; + } else { + std::cerr << "[ERROR] Plugin path must be provided via:" << std::endl; + std::cerr << " 1. Command line argument: ./plugin_loader_test " << std::endl; + std::cerr << " 2. Environment variable: export AXON_ROS2_PLUGIN_PATH=" << std::endl; + return 1; } + int test_duration_seconds = 10; + if (argc > 2) { test_duration_seconds = std::stoi(argv[2]); } diff --git a/apps/plugin_example/run_test_with_ros2_tools.sh b/apps/plugin_example/run_test_with_ros2_tools.sh new file mode 100644 index 0000000..a3ac02d --- /dev/null +++ b/apps/plugin_example/run_test_with_ros2_tools.sh @@ -0,0 +1,104 @@ +#!/bin/bash +# Demo script showing how to test the ROS2 plugin with ros2 topic pub CLI tool + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" + +# Plugin path - use environment variable, command line argument, or default relative path +if [ -n "$AXON_ROS2_PLUGIN_PATH" ]; then + PLUGIN_PATH="$AXON_ROS2_PLUGIN_PATH" +elif [ -n "$1" ]; then + PLUGIN_PATH="$1" +else + # Default relative path from project root + PLUGIN_PATH="${PROJECT_ROOT}/middlewares/ros2/install/axon_ros2_plugin/lib/axon/plugins/libaxon_ros2_plugin.so" +fi + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo -e "${GREEN}========================================" +echo " ROS2 Plugin CLI Tools Test" +echo "========================================${NC}" +echo "" +echo "This test will:" +echo " 1. Start the plugin subscriber in background" +echo " 2. Publish messages using 'ros2 topic pub'" +echo " 3. Verify the plugin receives the messages" +echo "" +echo "Plugin path: $PLUGIN_PATH" +echo "" + +# Source ROS2 +source /opt/ros/humble/setup.bash + +cd "$SCRIPT_DIR" + +# Check if plugin exists +if [ ! -f "$PLUGIN_PATH" ]; then + echo -e "${RED}[ERROR] Plugin not found: $PLUGIN_PATH${NC}" + exit 1 +fi + +# Start the subscriber in background +echo -e "${YELLOW}[INFO] Starting plugin subscriber...${NC}" +./build/test_with_ros2_tools "$PLUGIN_PATH" 20 > /tmp/plugin_test_output.txt 2>&1 & +SUBSCRIBER_PID=$! + +# Wait for plugin to initialize +sleep 2 + +# Check if subscriber is still running +if ! kill -0 $SUBSCRIBER_PID 2>/dev/null; then + echo -e "${RED}[ERROR] Subscriber failed to start${NC}" + cat /tmp/plugin_test_output.txt + exit 1 +fi + +echo -e "${GREEN}[OK] Subscriber started (PID: $SUBSCRIBER_PID)${NC}" +echo "" + +# Publish some messages +echo -e "${YELLOW}========================================" +echo " Publishing Messages" +echo "========================================${NC}" +echo "" + +for i in {1..5}; do + echo -e "${YELLOW}[PUBLISH] Sending message $i...${NC}" + ros2 topic pub --once /chatter std_msgs/String "data: 'Hello from CLI $i'" >/dev/null 2>&1 & + sleep 0.5 +done + +echo "" +echo -e "${GREEN}[INFO] Published 5 messages${NC}" +echo "" + +# Wait for subscriber to process all messages +echo -e "${YELLOW}[INFO] Waiting for subscriber to finish (20 seconds)...${NC}" +wait $SUBSCRIBER_PID 2>/dev/null || true +EXIT_CODE=$? + +# Display results +cat /tmp/plugin_test_output.txt + +echo "" +echo -e "${GREEN}========================================" +echo " Test Complete" +echo "========================================${NC}" +echo "" + +# Check if any messages were received +if grep -q "MSG 1" /tmp/plugin_test_output.txt; then + MESSAGE_COUNT=$(grep -c "^\\[MSG" /tmp/plugin_test_output.txt || echo "0") + echo -e "${GREEN}[SUCCESS] Plugin received $MESSAGE_COUNT messages!${NC}" + exit 0 +else + echo -e "${RED}[FAIL] No messages received${NC}" + exit 1 +fi diff --git a/apps/plugin_example/test_with_ros2_tools.cpp b/apps/plugin_example/test_with_ros2_tools.cpp new file mode 100644 index 0000000..3629be2 --- /dev/null +++ b/apps/plugin_example/test_with_ros2_tools.cpp @@ -0,0 +1,183 @@ +/** + * @file test_with_ros2_tools.cpp + * @brief Test plugin by subscribing to messages published via ros2 topic pub CLI tool + * + * Usage: + * 1. Run this program: ./test_with_ros2_tools + * 2. In another terminal, publish messages: + * ros2 topic pub /chatter std_msgs/String "data: 'Hello World'" + */ + +#include +#include +#include +#include +#include + +#include "../axon_recorder/plugin_loader.hpp" + +using namespace axon; + +static std::sig_atomic_t g_running = 1; +static std::atomic g_message_count{0}; + +void signal_handler(int signal) { + (void)signal; + std::cout << "\n[INFO] Shutdown signal received..." << std::endl; + g_running = 0; +} + +void message_callback( + const char* topic_name, const uint8_t* message_data, size_t message_size, + const char* message_type, uint64_t timestamp, void* user_data +) { + (void)user_data; + g_message_count++; + + std::cout << "[MSG " << g_message_count << "] Topic: " << topic_name + << " | Type: " << message_type << " | Size: " << message_size << " bytes" + << " | Timestamp: " << timestamp << std::endl; + + // Print first 16 bytes + constexpr size_t MAX_DUMP = 16; + size_t dump_size = std::min(message_size, MAX_DUMP); + std::cout << " Data: "; + for (size_t i = 0; i < dump_size; ++i) { + printf("%02x ", message_data[i]); + } + if (message_size > MAX_DUMP) { + std::cout << "..."; + } + std::cout << std::endl; +} + +int main(int argc, char* argv[]) { + std::cout << "========================================" << std::endl; + std::cout << " ROS2 Plugin Test with CLI Tools" << std::endl; + std::cout << "========================================" << std::endl; + + // Parse arguments + // Default plugin path - can be overridden via command line argument or environment variable + std::string plugin_path; + const char* env_path = std::getenv("AXON_ROS2_PLUGIN_PATH"); + if (env_path) { + plugin_path = env_path; + } else if (argc > 1) { + plugin_path = argv[1]; + } else { + std::cerr << "[ERROR] Plugin path must be provided via:" << std::endl; + std::cerr << " 1. Command line argument: ./test_with_ros2_tools " << std::endl; + std::cerr << " 2. Environment variable: export AXON_ROS2_PLUGIN_PATH=" << std::endl; + return 1; + } + int wait_seconds = 30; + + if (argc > 2) { + wait_seconds = std::atoi(argv[2]); + } + + std::cout << "[INFO] Plugin path: " << plugin_path << std::endl; + std::cout << "[INFO] Wait time: " << wait_seconds << " seconds" << std::endl; + + // Setup signal handler + std::signal(SIGINT, signal_handler); + std::signal(SIGTERM, signal_handler); + + // Create loader + PluginLoader loader; + + // Load plugin + std::cout << "\n[INFO] Loading plugin..." << std::endl; + auto plugin_name_opt = loader.load(plugin_path); + + if (!plugin_name_opt) { + std::cerr << "[ERROR] Failed to load plugin: " << loader.get_last_error() << std::endl; + return 1; + } + + std::string plugin_name = *plugin_name_opt; + std::cout << "[OK] Loaded plugin: " << plugin_name << std::endl; + + // Initialize plugin + const auto* descriptor = loader.get_descriptor(plugin_name); + auto* plugin = loader.get_plugin(plugin_name); + + const char* config_json = "{}"; + AxonStatus status = descriptor->vtable->init(config_json); + + if (status != AXON_SUCCESS) { + std::cerr << "[ERROR] Failed to initialize plugin, status: " << status << std::endl; + loader.unload_all(); + return 1; + } + + plugin->initialized = true; + std::cout << "[OK] Plugin initialized" << std::endl; + + // Subscribe to topic + std::cout << "[INFO] Subscribing to /chatter..." << std::endl; + status = + descriptor->vtable->subscribe("/chatter", "std_msgs/msg/String", message_callback, nullptr); + + if (status != AXON_SUCCESS) { + std::cerr << "[ERROR] Failed to subscribe, status: " << status << std::endl; + loader.unload_all(); + return 1; + } + + std::cout << "[OK] Subscribed to /chatter" << std::endl; + + // Start spinning + std::cout << "[INFO] Starting plugin..." << std::endl; + status = descriptor->vtable->start(); + + if (status != AXON_SUCCESS) { + std::cerr << "[ERROR] Failed to start plugin, status: " << status << std::endl; + loader.unload_all(); + return 1; + } + + plugin->running = true; + std::cout << "[OK] Plugin started" << std::endl; + + // Wait for messages + std::cout << "\n========================================" << std::endl; + std::cout << " Ready to receive messages" << std::endl; + std::cout << "========================================" << std::endl; + std::cout << "\nIn another terminal, run:" << std::endl; + std::cout << " ros2 topic pub /chatter std_msgs/String \"data: 'Hello World'\"" << std::endl; + std::cout << "\nWaiting " << wait_seconds << " seconds (or press Ctrl+C to stop early)..." + << std::endl; + std::cout << std::endl; + + auto start_time = std::chrono::steady_clock::now(); + while (g_running) { + auto elapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now() - start_time + ) + .count(); + + if (elapsed >= wait_seconds) { + break; + } + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + // Shutdown + std::cout << "\n[INFO] Shutting down..." << std::endl; + if (descriptor->vtable->stop) { + descriptor->vtable->stop(); + } + plugin->running = false; + + loader.unload_all(); + std::cout << "[OK] Plugin unloaded" << std::endl; + + std::cout << "\n========================================" << std::endl; + std::cout << " Test Complete" << std::endl; + std::cout << " Messages received: " << g_message_count << std::endl; + std::cout << "========================================" << std::endl; + + return (g_message_count > 0) ? 0 : 1; +} diff --git a/middlewares/mock/src/mock_plugin/CMakeLists.txt b/middlewares/mock/src/mock_plugin/CMakeLists.txt new file mode 100644 index 0000000..5ed9210 --- /dev/null +++ b/middlewares/mock/src/mock_plugin/CMakeLists.txt @@ -0,0 +1,96 @@ +cmake_minimum_required(VERSION 3.12) +project(mock_plugin) + +# Default to C++17 +if(NOT CMAKE_CXX_STANDARD) + set(CMAKE_CXX_STANDARD 17) +endif() + +if(NOT CMAKE_CXX_STANDARD_REQUIRED) + set(CMAKE_CXX_STANDARD_REQUIRED ON) +endif() + +# Compiler warnings +if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + add_compile_options(-Wall -Wextra -Wpedantic) +endif() + +# Build the mock plugin as a shared library +add_library(${PROJECT_NAME} SHARED + src/mock_plugin.cpp + src/mock_plugin_export.cpp +) + +target_include_directories(${PROJECT_NAME} PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/include +) + +# Set library properties +set_target_properties(${PROJECT_NAME} PROPERTIES + VERSION 1.0.0 + SOVERSION 1 + CXX_VISIBILITY_PRESET default + VISIBILITY_INLINES_HIDDEN ON +) + +# Link pthread (required for std::thread) +target_link_libraries(${PROJECT_NAME} + pthread +) + +# Also create an object library for testing +add_library(${PROJECT_NAME}_obj OBJECT + src/mock_plugin.cpp +) + +target_include_directories(${PROJECT_NAME}_obj PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/include +) + +# Install +install(TARGETS ${PROJECT_NAME} + LIBRARY DESTINATION lib + ARCHIVE DESTINATION lib +) + +install(FILES include/mock_plugin.hpp + DESTINATION include +) + +# ============================================================================= +# Tests +# ============================================================================= + +# Simple plugin load test (requires building core libs first) +add_executable(test_mock_plugin_load + test/test_mock_plugin_load.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../apps/axon_recorder/plugin_loader.cpp +) + +target_include_directories(test_mock_plugin_load PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/include + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../apps/axon_recorder +) + +target_link_libraries(test_mock_plugin_load + ${CMAKE_DL_LIBS} +) + +# E2E test with mock plugin +add_executable(test_mock_plugin_e2e + test/test_mock_plugin_e2e.cpp +) + +target_include_directories(test_mock_plugin_e2e PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/include +) + +target_link_libraries(test_mock_plugin_e2e + ${PROJECT_NAME}_obj + pthread +) + +# Install tests +install(TARGETS test_mock_plugin_load test_mock_plugin_e2e + RUNTIME DESTINATION bin +) diff --git a/middlewares/mock/src/mock_plugin/include/mock_plugin.hpp b/middlewares/mock/src/mock_plugin/include/mock_plugin.hpp new file mode 100644 index 0000000..a60846d --- /dev/null +++ b/middlewares/mock/src/mock_plugin/include/mock_plugin.hpp @@ -0,0 +1,83 @@ +#ifndef MOCK_PLUGIN_HPP +#define MOCK_PLUGIN_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mock_plugin { + +// Message callback signature +using MessageCallback = std::function& message_data, + const std::string& message_type, uint64_t timestamp +)>; + +// Mock subscription info +struct MockSubscription { + std::string topic_name; + std::string message_type; + MessageCallback callback; + void* user_data; + int publish_count; // Track how many messages were published +}; + +// Mock plugin implementation +class MockPlugin { +public: + MockPlugin(); + ~MockPlugin(); + + // Initialize the plugin with config (JSON string, but we'll ignore it for mock) + bool init(const char* config_json); + + // Start the mock plugin (spins a thread that generates fake messages) + bool start(); + + // Stop the plugin + bool stop(); + + // Subscribe to a topic + bool subscribe( + const std::string& topic_name, const std::string& message_type, MessageCallback callback, + void* user_data + ); + + // Publish a message (for testing purposes) + bool publish( + const std::string& topic_name, const std::vector& data, const std::string& message_type + ); + + // Check if plugin is running + bool is_running() const; + + // Get number of active subscriptions + size_t get_subscription_count() const; + + // Publish mock messages to all subscriptions + void publish_mock_messages(); + +private: + mutable std::mutex mutex_; + std::atomic running_; + std::atomic initialized_; + std::thread publisher_thread_; + + // Map of topic name to subscription info + std::unordered_map subscriptions_; + + // Publisher thread function + void publisher_loop(); + + // Generate mock message data for a given message type + std::vector generate_mock_message(const std::string& message_type); +}; + +} // namespace mock_plugin + +#endif // MOCK_PLUGIN_HPP diff --git a/middlewares/mock/src/mock_plugin/src/mock_plugin.cpp b/middlewares/mock/src/mock_plugin/src/mock_plugin.cpp new file mode 100644 index 0000000..bcc7a3b --- /dev/null +++ b/middlewares/mock/src/mock_plugin/src/mock_plugin.cpp @@ -0,0 +1,190 @@ +#include "mock_plugin.hpp" + +#include +#include +#include +#include + +namespace mock_plugin { + +MockPlugin::MockPlugin() + : running_(false) + , initialized_(false) {} + +MockPlugin::~MockPlugin() { + stop(); +} + +bool MockPlugin::init(const char* config_json) { + (void)config_json; // Ignore config for mock + + std::lock_guard lock(mutex_); + + if (initialized_) { + std::cerr << "MockPlugin: Already initialized" << std::endl; + return false; + } + + initialized_ = true; + std::cout << "MockPlugin: Initialized" << std::endl; + return true; +} + +bool MockPlugin::start() { + std::lock_guard lock(mutex_); + + if (!initialized_) { + std::cerr << "MockPlugin: Not initialized" << std::endl; + return false; + } + + if (running_) { + std::cerr << "MockPlugin: Already running" << std::endl; + return false; + } + + running_ = true; + + // Start publisher thread + publisher_thread_ = std::thread(&MockPlugin::publisher_loop, this); + + std::cout << "MockPlugin: Started" << std::endl; + return true; +} + +bool MockPlugin::stop() { + { + std::lock_guard lock(mutex_); + if (!running_) { + return true; // Already stopped + } + running_ = false; + } + + // Wait for publisher thread to finish + if (publisher_thread_.joinable()) { + publisher_thread_.join(); + } + + std::cout << "MockPlugin: Stopped" << std::endl; + return true; +} + +bool MockPlugin::subscribe( + const std::string& topic_name, const std::string& message_type, MessageCallback callback, + void* user_data +) { + std::lock_guard lock(mutex_); + + if (subscriptions_.find(topic_name) != subscriptions_.end()) { + std::cerr << "MockPlugin: Already subscribed to " << topic_name << std::endl; + return false; + } + + MockSubscription sub; + sub.topic_name = topic_name; + sub.message_type = message_type; + sub.callback = callback; + sub.user_data = user_data; + sub.publish_count = 0; + + subscriptions_[topic_name] = sub; + + std::cout << "MockPlugin: Subscribed to " << topic_name << " (" << message_type << ")" + << std::endl; + return true; +} + +bool MockPlugin::publish( + const std::string& topic_name, const std::vector& data, const std::string& message_type +) { + std::lock_guard lock(mutex_); + + auto it = subscriptions_.find(topic_name); + if (it == subscriptions_.end()) { + std::cerr << "MockPlugin: No subscription for " << topic_name << std::endl; + return false; + } + + // Get current timestamp + auto now = std::chrono::system_clock::now(); + auto timestamp = + std::chrono::duration_cast(now.time_since_epoch()).count(); + + // Call the callback + it->second.callback(topic_name, data, message_type, timestamp); + it->second.publish_count++; + + return true; +} + +bool MockPlugin::is_running() const { + return running_; +} + +size_t MockPlugin::get_subscription_count() const { + std::lock_guard lock(mutex_); + return subscriptions_.size(); +} + +void MockPlugin::publish_mock_messages() { + std::lock_guard lock(mutex_); + + auto now = std::chrono::system_clock::now(); + auto timestamp = + std::chrono::duration_cast(now.time_since_epoch()).count(); + + // Publish a mock message to each subscription + for (auto& [topic_name, sub] : subscriptions_) { + auto mock_data = generate_mock_message(sub.message_type); + sub.callback(topic_name, mock_data, sub.message_type, timestamp); + sub.publish_count++; + } +} + +void MockPlugin::publisher_loop() { + std::cout << "MockPlugin: Publisher thread started" << std::endl; + + while (running_) { + // Publish mock messages every 100ms + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + if (running_) { + publish_mock_messages(); + } + } + + std::cout << "MockPlugin: Publisher thread stopped" << std::endl; +} + +std::vector MockPlugin::generate_mock_message(const std::string& message_type) { + // Generate simple mock data based on message type + std::vector data; + + if (message_type == "std_msgs/String") { + // Mock string message: "Hello, Mock!" + std::string msg = "Hello, Mock!"; + data.insert(data.end(), msg.begin(), msg.end()); + } else if (message_type == "std_msgs/Int32") { + // Mock int32 message: 42 + int32_t value = 42; + data.resize(sizeof(int32_t)); + std::memcpy(data.data(), &value, sizeof(int32_t)); + } else if (message_type == "sensor_msgs/Image") { + // Mock image header + std::string header = "MockImage"; + data.insert(data.end(), header.begin(), header.end()); + // Add some fake image data (100 bytes) + for (int i = 0; i < 100; i++) { + data.push_back(static_cast(i % 256)); + } + } else { + // Generic mock data + std::string msg = "Mock data for " + message_type; + data.insert(data.end(), msg.begin(), msg.end()); + } + + return data; +} + +} // namespace mock_plugin diff --git a/middlewares/mock/src/mock_plugin/src/mock_plugin_export.cpp b/middlewares/mock/src/mock_plugin/src/mock_plugin_export.cpp new file mode 100644 index 0000000..d5a702f --- /dev/null +++ b/middlewares/mock/src/mock_plugin/src/mock_plugin_export.cpp @@ -0,0 +1,215 @@ +// Mock Plugin C ABI Export +// Implements the Axon plugin C ABI interface for testing + +#include +#include +#include +#include +#include +#include + +#include "mock_plugin.hpp" + +using namespace mock_plugin; + +// ============================================================================= +// Error codes (matching the Axon plugin ABI) +// ============================================================================= +enum AxonStatus : int32_t { + AXON_SUCCESS = 0, + AXON_ERROR_INVALID_ARGUMENT = -1, + AXON_ERROR_NOT_INITIALIZED = -2, + AXON_ERROR_ALREADY_INITIALIZED = -3, + AXON_ERROR_NOT_STARTED = -4, + AXON_ERROR_ALREADY_STARTED = -5, + AXON_ERROR_INTERNAL = -100, +}; + +// ============================================================================= +// Message callback type +// ============================================================================= +extern "C" { + +using AxonMessageCallback = void (*)( + const char* topic_name, const uint8_t* message_data, size_t message_size, + const char* message_type, uint64_t timestamp, void* user_data +); + +// ============================================================================= +// Global plugin state +// ============================================================================= +static std::unique_ptr g_plugin = nullptr; +static std::mutex g_plugin_mutex; + +// ============================================================================= +// C API Implementation +// ============================================================================= + +// Initialize the mock plugin +static int32_t axon_init(const char* config_json) { + std::lock_guard lock(g_plugin_mutex); + + if (g_plugin) { + std::cerr << "Mock plugin already initialized" << std::endl; + return static_cast(AXON_ERROR_ALREADY_INITIALIZED); + } + + try { + g_plugin = std::make_unique(); + + if (!g_plugin->init(config_json)) { + g_plugin.reset(); + return static_cast(AXON_ERROR_INTERNAL); + } + + std::cout << "Mock plugin initialized via C API" << std::endl; + return static_cast(AXON_SUCCESS); + + } catch (const std::exception& e) { + std::cerr << "Failed to initialize mock plugin: " << e.what() << std::endl; + g_plugin.reset(); + return static_cast(AXON_ERROR_INTERNAL); + } +} + +// Start the mock plugin +static int32_t axon_start(void) { + std::lock_guard lock(g_plugin_mutex); + + if (!g_plugin) { + std::cerr << "Mock plugin not initialized" << std::endl; + return static_cast(AXON_ERROR_NOT_INITIALIZED); + } + + if (!g_plugin->start()) { + return static_cast(AXON_ERROR_INTERNAL); + } + + std::cout << "Mock plugin started via C API" << std::endl; + return static_cast(AXON_SUCCESS); +} + +// Stop the mock plugin +static int32_t axon_stop(void) { + std::lock_guard lock(g_plugin_mutex); + + if (!g_plugin) { + return static_cast(AXON_SUCCESS); // Already stopped + } + + if (!g_plugin->stop()) { + return static_cast(AXON_ERROR_INTERNAL); + } + + g_plugin.reset(); + + std::cout << "Mock plugin stopped via C API" << std::endl; + return static_cast(AXON_SUCCESS); +} + +// Subscribe to a topic with callback +static int32_t axon_subscribe( + const char* topic_name, const char* message_type, AxonMessageCallback callback, void* user_data +) { + if (!topic_name || !message_type || !callback) { + return static_cast(AXON_ERROR_INVALID_ARGUMENT); + } + + std::lock_guard lock(g_plugin_mutex); + + if (!g_plugin) { + std::cerr << "Cannot subscribe: mock plugin not initialized" << std::endl; + return static_cast(AXON_ERROR_NOT_INITIALIZED); + } + + // Create lambda wrapper for the callback + MessageCallback wrapper = [callback, user_data]( + const std::string& topic, + const std::vector& data, + const std::string& type, + uint64_t timestamp + ) { + callback(topic.c_str(), data.data(), data.size(), type.c_str(), timestamp, user_data); + }; + + if (!g_plugin->subscribe( + std::string(topic_name), std::string(message_type), wrapper, user_data + )) { + return static_cast(AXON_ERROR_INTERNAL); + } + + return static_cast(AXON_SUCCESS); +} + +// Publish a message +static int32_t axon_publish( + const char* topic_name, const uint8_t* message_data, size_t message_size, const char* message_type +) { + if (!topic_name || !message_data || !message_type) { + return static_cast(AXON_ERROR_INVALID_ARGUMENT); + } + + std::lock_guard lock(g_plugin_mutex); + + if (!g_plugin) { + std::cerr << "Cannot publish: mock plugin not initialized" << std::endl; + return static_cast(AXON_ERROR_NOT_INITIALIZED); + } + + std::vector data(message_data, message_data + message_size); + + if (!g_plugin->publish(std::string(topic_name), data, std::string(message_type))) { + return static_cast(AXON_ERROR_INTERNAL); + } + + return static_cast(AXON_SUCCESS); +} + +// ============================================================================= +// Plugin descriptor export +// ============================================================================= + +// Version information +#define AXON_ABI_VERSION_MAJOR 1 +#define AXON_ABI_VERSION_MINOR 0 + +// Plugin vtable structure (matching loader's expectation) +struct AxonPluginVtable { + int32_t (*init)(const char*); + int32_t (*start)(void); + int32_t (*stop)(void); + int32_t (*subscribe)(const char*, const char*, AxonMessageCallback, void*); + int32_t (*publish)(const char*, const uint8_t*, size_t, const char*); + void* reserved[9]; +}; + +// Plugin descriptor structure (matching loader's expectation) +struct AxonPluginDescriptor { + uint32_t abi_version_major; + uint32_t abi_version_minor; + const char* middleware_name; + const char* middleware_version; + const char* plugin_version; + AxonPluginVtable* vtable; + void* reserved[16]; +}; + +// Static vtable +static AxonPluginVtable mock_vtable = { + axon_init, axon_start, axon_stop, axon_subscribe, axon_publish, {nullptr}}; + +// Exported plugin descriptor +__attribute__((visibility("default"))) const AxonPluginDescriptor* axon_get_plugin_descriptor(void +) { + static const AxonPluginDescriptor descriptor = { + AXON_ABI_VERSION_MAJOR, + AXON_ABI_VERSION_MINOR, + "Mock", + "1.0.0", + "1.0.0", + &mock_vtable, + {nullptr}}; + return &descriptor; +} + +} // extern "C" diff --git a/middlewares/mock/src/mock_plugin/test/test_mock_plugin_e2e.cpp b/middlewares/mock/src/mock_plugin/test/test_mock_plugin_e2e.cpp new file mode 100644 index 0000000..f59af13 --- /dev/null +++ b/middlewares/mock/src/mock_plugin/test/test_mock_plugin_e2e.cpp @@ -0,0 +1,171 @@ +/** + * @file test_mock_plugin_e2e.cpp + * @brief End-to-end test for the mock plugin without PluginLoader + * This tests the plugin functionality directly without loading via dlopen + */ + +#include +#include +#include +#include +#include +#include + +#include "mock_plugin.hpp" + +using namespace mock_plugin; + +int main(int argc, char* argv[]) { + (void)argc; + (void)argv; + + std::cout << "=== Mock Plugin E2E Test ===" << std::endl; + + // Test 1: Create plugin + std::cout << "\n[TEST 1] Creating mock plugin..." << std::endl; + MockPlugin plugin; + std::cout << "[PASS] Plugin created" << std::endl; + + // Test 2: Initialize + std::cout << "\n[TEST 2] Initializing plugin..." << std::endl; + if (!plugin.init("{}")) { + std::cerr << "[FAIL] Could not initialize plugin" << std::endl; + return 1; + } + std::cout << "[PASS] Plugin initialized" << std::endl; + + // Test 3: Start + std::cout << "\n[TEST 3] Starting plugin..." << std::endl; + if (!plugin.start()) { + std::cerr << "[FAIL] Could not start plugin" << std::endl; + return 1; + } + std::cout << "[PASS] Plugin started" << std::endl; + + // Test 4: Check if running + std::cout << "\n[TEST 4] Checking if running..." << std::endl; + if (!plugin.is_running()) { + std::cerr << "[FAIL] Plugin should be running" << std::endl; + return 1; + } + std::cout << "[PASS] Plugin is running" << std::endl; + + // Test 5: Subscribe to topics + std::cout << "\n[TEST 5] Subscribing to topics..." << std::endl; + + std::atomic string_count{0}; + std::atomic int_count{0}; + std::atomic image_count{0}; + + auto string_callback = [&string_count]( + const std::string& topic, + const std::vector& data, + const std::string& type, + uint64_t timestamp + ) { + string_count++; + std::string msg(data.begin(), data.end()); + std::cout << " [String #" << string_count << "] " << topic << " (" << type << "): " << msg + << " @ " << timestamp << std::endl; + }; + + auto int_callback = [&int_count]( + const std::string& topic, + const std::vector& data, + const std::string& type, + uint64_t timestamp + ) { + int_count++; + if (data.size() >= sizeof(int32_t)) { + int32_t value; + std::memcpy(&value, data.data(), sizeof(int32_t)); + std::cout << " [Int #" << int_count << "] " << topic << " (" << type << "): " << value + << " @ " << timestamp << std::endl; + } + }; + + auto image_callback = [&image_count]( + const std::string& topic, + const std::vector& data, + const std::string& type, + uint64_t timestamp + ) { + image_count++; + std::cout << " [Image #" << image_count << "] " << topic << " (" << type + << "): " << data.size() << " bytes @ " << timestamp << std::endl; + }; + + if (!plugin.subscribe("/test/string", "std_msgs/String", string_callback, nullptr)) { + std::cerr << "[FAIL] Could not subscribe to /test/string" << std::endl; + return 1; + } + std::cout << " ✓ Subscribed to /test/string (std_msgs/String)" << std::endl; + + if (!plugin.subscribe("/test/int", "std_msgs/Int32", int_callback, nullptr)) { + std::cerr << "[FAIL] Could not subscribe to /test/int" << std::endl; + return 1; + } + std::cout << " ✓ Subscribed to /test/int (std_msgs/Int32)" << std::endl; + + if (!plugin.subscribe("/test/image", "sensor_msgs/Image", image_callback, nullptr)) { + std::cerr << "[FAIL] Could not subscribe to /test/image" << std::endl; + return 1; + } + std::cout << " ✓ Subscribed to /test/image (sensor_msgs/Image)" << std::endl; + + // Test 6: Check subscription count + std::cout << "\n[TEST 6] Checking subscription count..." << std::endl; + size_t sub_count = plugin.get_subscription_count(); + if (sub_count != 3) { + std::cerr << "[FAIL] Expected 3 subscriptions, got " << sub_count << std::endl; + return 1; + } + std::cout << "[PASS] Subscription count: " << sub_count << std::endl; + + // Test 7: Receive messages + std::cout << "\n[TEST 7] Receiving messages (2 seconds)..." << std::endl; + std::this_thread::sleep_for(std::chrono::seconds(2)); + std::cout << "[PASS] Messages received:" << std::endl; + std::cout << " String messages: " << string_count << std::endl; + std::cout << " Int messages: " << int_count << std::endl; + std::cout << " Image messages: " << image_count << std::endl; + + if (string_count == 0 || int_count == 0 || image_count == 0) { + std::cerr << "[FAIL] Not all message types received!" << std::endl; + return 1; + } + + // Test 8: Publish manually + std::cout << "\n[TEST 8] Publishing messages manually..." << std::endl; + + std::string test_data = "Manual test message"; + std::vector manual_data(test_data.begin(), test_data.end()); + + if (!plugin.publish("/test/string", manual_data, "std_msgs/String")) { + std::cerr << "[FAIL] Could not publish manual message" << std::endl; + return 1; + } + std::cout << "[PASS] Manual message published" << std::endl; + + // Wait for the manual message + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + // Test 9: Stop plugin + std::cout << "\n[TEST 9] Stopping plugin..." << std::endl; + if (!plugin.stop()) { + std::cerr << "[FAIL] Could not stop plugin" << std::endl; + return 1; + } + std::cout << "[PASS] Plugin stopped" << std::endl; + + // Test 10: Check if stopped + std::cout << "\n[TEST 10] Checking if stopped..." << std::endl; + if (plugin.is_running()) { + std::cerr << "[FAIL] Plugin should not be running" << std::endl; + return 1; + } + std::cout << "[PASS] Plugin is stopped" << std::endl; + + std::cout << "\n=== All Tests PASSED ===" << std::endl; + return 0; +} diff --git a/middlewares/mock/src/mock_plugin/test/test_mock_plugin_load.cpp b/middlewares/mock/src/mock_plugin/test/test_mock_plugin_load.cpp new file mode 100644 index 0000000..26e8f11 --- /dev/null +++ b/middlewares/mock/src/mock_plugin/test/test_mock_plugin_load.cpp @@ -0,0 +1,159 @@ +/** + * @file test_mock_plugin_load.cpp + * @brief Test loading the mock plugin using PluginLoader + */ + +#include +#include +#include + +#include "plugin_loader.hpp" + +using namespace axon; + +int main(int argc, char* argv[]) { + std::cout << "=== Mock Plugin Load Test ===" << std::endl; + + // Parse arguments + if (argc < 2) { + std::cerr << "Usage: " << argv[0] << " " << std::endl; + std::cerr << "Example: " << argv[0] << " /path/to/libmock_plugin.so" << std::endl; + return 1; + } + + std::string plugin_path = argv[1]; + std::cout << "Plugin path: " << plugin_path << std::endl << std::endl; + + // Create loader + PluginLoader loader; + + // Test 1: Load plugin + std::cout << "[TEST 1] Loading plugin..." << std::endl; + auto plugin_name_opt = loader.load(plugin_path); + if (!plugin_name_opt) { + std::cerr << "[FAIL] Could not load plugin" << std::endl; + std::cerr << "Error: " << loader.get_last_error() << std::endl; + return 1; + } + std::cout << "[PASS] Plugin loaded: " << *plugin_name_opt << std::endl << std::endl; + + // Test 2: Get descriptor + std::cout << "[TEST 2] Getting descriptor..." << std::endl; + const auto* descriptor = loader.get_descriptor(*plugin_name_opt); + if (!descriptor) { + std::cerr << "[FAIL] Could not get descriptor" << std::endl; + return 1; + } + std::cout << "[PASS] Descriptor found" << std::endl; + + // Print plugin info + std::cout << "\nPlugin Information:" << std::endl; + std::cout << " ABI Version: " << descriptor->abi_version_major << "." + << descriptor->abi_version_minor << std::endl; + std::cout << " Middleware: " + << (descriptor->middleware_name ? descriptor->middleware_name : "N/A") << std::endl; + std::cout << " Middleware Version: " + << (descriptor->middleware_version ? descriptor->middleware_version : "N/A") + << std::endl; + std::cout << " Plugin Version: " + << (descriptor->plugin_version ? descriptor->plugin_version : "N/A") << std::endl; + + // Test 3: Check vtable + std::cout << "\n[TEST 3] Checking vtable..." << std::endl; + if (!descriptor->vtable) { + std::cerr << "[FAIL] Vtable is null" << std::endl; + return 1; + } + std::cout << "[PASS] Vtable exists" << std::endl; + + std::cout << "\nVtable Functions:" << std::endl; + std::cout << " init: " << (descriptor->vtable->init ? "✓" : "✗") << std::endl; + std::cout << " start: " << (descriptor->vtable->start ? "✓" : "✗") << std::endl; + std::cout << " stop: " << (descriptor->vtable->stop ? "✓" : "✗") << std::endl; + std::cout << " subscribe: " << (descriptor->vtable->subscribe ? "✓" : "✗") << std::endl; + std::cout << " publish: " << (descriptor->vtable->publish ? "✓" : "✗") << std::endl; + + // Test 4: Initialize plugin + std::cout << "\n[TEST 4] Initializing plugin..." << std::endl; + if (descriptor->vtable->init("{}") != 0) { + std::cerr << "[FAIL] Could not initialize plugin" << std::endl; + return 1; + } + std::cout << "[PASS] Plugin initialized" << std::endl; + + // Test 5: Start plugin + std::cout << "\n[TEST 5] Starting plugin..." << std::endl; + if (descriptor->vtable->start() != 0) { + std::cerr << "[FAIL] Could not start plugin" << std::endl; + return 1; + } + std::cout << "[PASS] Plugin started" << std::endl; + + // Test 6: Subscribe to a topic + std::cout << "\n[TEST 6] Subscribing to topic..." << std::endl; + int message_count = 0; + + auto callback = []( + const char* topic_name, + const uint8_t* data, + size_t size, + const char* type, + uint64_t timestamp, + void* user_data + ) { + int* count = static_cast(user_data); + (*count)++; + + std::cout << " [Message #" << *count << "]" << std::endl; + std::cout << " Topic: " << topic_name << std::endl; + std::cout << " Type: " << type << std::endl; + std::cout << " Size: " << size << " bytes" << std::endl; + std::cout << " Timestamp: " << timestamp << std::endl; + + // Print first few bytes of data + std::cout << " Data: "; + size_t preview_len = std::min(size_t(32), size); + for (size_t i = 0; i < preview_len; i++) { + printf("%02x ", data[i]); + } + if (size > 32) { + std::cout << "..."; + } + std::cout << std::endl; + }; + + if (descriptor->vtable->subscribe("/test_topic", "std_msgs/String", callback, &message_count) != 0) { + std::cerr << "[FAIL] Could not subscribe to topic" << std::endl; + return 1; + } + std::cout << "[PASS] Subscribed to /test_topic" << std::endl; + + // Test 7: Receive messages + std::cout << "\n[TEST 7] Receiving messages (2 seconds)..." << std::endl; + std::this_thread::sleep_for(std::chrono::seconds(2)); + std::cout << "[PASS] Received " << message_count << " messages" << std::endl; + + if (message_count == 0) { + std::cerr << "[FAIL] No messages received!" << std::endl; + return 1; + } + + // Test 8: Stop plugin + std::cout << "\n[TEST 8] Stopping plugin..." << std::endl; + if (descriptor->vtable->stop() != 0) { + std::cerr << "[FAIL] Could not stop plugin" << std::endl; + return 1; + } + std::cout << "[PASS] Plugin stopped" << std::endl; + + // Test 9: Unload + std::cout << "\n[TEST 9] Unloading plugin..." << std::endl; + if (!loader.unload(*plugin_name_opt)) { + std::cerr << "[FAIL] Could not unload plugin" << std::endl; + return 1; + } + std::cout << "[PASS] Plugin unloaded" << std::endl; + + std::cout << "\n=== All Tests PASSED ===" << std::endl; + return 0; +} diff --git a/middlewares/mock/test_e2e_with_mock.sh b/middlewares/mock/test_e2e_with_mock.sh new file mode 100644 index 0000000..4a96ff8 --- /dev/null +++ b/middlewares/mock/test_e2e_with_mock.sh @@ -0,0 +1,466 @@ +#!/bin/bash +# @file test_e2e_with_mock.sh +# @brief E2E test for mock middleware integration with axon_recorder +# +# This script tests the complete recording workflow using the mock middleware: +# 1. Build mock middleware (if needed) +# 2. Build axon_recorder (if needed) +# 3. Start axon_recorder with mock middleware +# 4. Send HTTP requests to control recording +# 5. Verify mock messages are received and recorded to MCAP +# 6. Verify sidecar JSON generation +# 7. Clean up test artifacts + +set -e + +# ============================================================================== +# Configuration +# ============================================================================== + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" +BUILD_DIR="${PROJECT_ROOT}/build" +TEST_DIR="${SCRIPT_DIR}/test_data" +MOCK_BUILD_DIR="${PROJECT_ROOT}/middlewares/mock/src/mock_plugin/build" + +# Mock plugin path +MOCK_PLUGIN="${MOCK_BUILD_DIR}/libmock_plugin.so" + +# Try multiple possible locations for axon_recorder binary +if [[ -f "${BUILD_DIR}/apps/axon_recorder/axon_recorder" ]]; then + RECORDER_BIN="${BUILD_DIR}/apps/axon_recorder/axon_recorder" +elif [[ -f "${PROJECT_ROOT}/apps/axon_recorder/build/axon_recorder" ]]; then + RECORDER_BIN="${PROJECT_ROOT}/apps/axon_recorder/build/axon_recorder" +else + RECORDER_BIN="${BUILD_DIR}/apps/axon_recorder/axon_recorder" +fi + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Test configuration +HTTP_PORT=8080 +TEST_TASK_ID="mock_e2e_test_$(date +%s)" +TEST_DEVICE_ID="mock_test_robot_01" +TEST_SCENE="mock_e2e_scene" + +# ============================================================================== +# Helper Functions +# ============================================================================== + +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +log_step() { + echo -e "${BLUE}[STEP]${NC} $1" +} + +cleanup() { + log_info "Cleaning up..." + + # Stop recorder if running + if [[ -n "${RECORDER_PID}" ]]; then + kill "${RECORDER_PID}" 2>/dev/null || true + wait "${RECORDER_PID}" 2>/dev/null || true + fi + + # Clean up test data (optional - comment out if you want to inspect files) + if [[ "${KEEP_TEST_DATA}" != "1" ]]; then + rm -rf "${TEST_DIR}" + else + log_info "Keeping test data in: ${TEST_DIR}" + fi + + log_info "Cleanup complete" +} + +# Trap to ensure cleanup on exit +trap cleanup EXIT INT TERM + +# ============================================================================== +# Build Functions +# ============================================================================== + +build_mock_middleware() { + if [[ -f "${MOCK_PLUGIN}" ]]; then + log_info "Mock middleware already built" + return 0 + fi + + log_step "Building mock middleware..." + cd "${PROJECT_ROOT}" + make build-mock + + if [[ ! -f "${MOCK_PLUGIN}" ]]; then + log_error "Failed to build mock middleware" + exit 1 + fi + + log_info "Mock middleware built successfully" +} + +build_axon_recorder() { + if [[ -f "${RECORDER_BIN}" ]]; then + log_info "axon_recorder already built" + return 0 + fi + + log_step "Building axon_recorder..." + cd "${PROJECT_ROOT}" + make build-core + + # Find the actual binary location + if [[ ! -f "${RECORDER_BIN}" ]]; then + RECORDER_BIN=$(find "${BUILD_DIR}" -name "axon_recorder" -type f -executable 2>/dev/null | head -n 1) + if [[ -z "${RECORDER_BIN}" ]]; then + log_error "Failed to build axon_recorder" + exit 1 + fi + fi + + log_info "axon_recorder built successfully: ${RECORDER_BIN}" +} + +# ============================================================================== +# Setup +# ============================================================================== + +setup() { + log_step "Setting up E2E test environment..." + + # Create test data directory + mkdir -p "${TEST_DIR}" + + # Build dependencies + build_mock_middleware + build_axon_recorder + + log_info "Using recorder: ${RECORDER_BIN}" + log_info "Using mock plugin: ${MOCK_PLUGIN}" + + log_info "Setup complete" +} + +# ============================================================================== +# Test Functions +# ============================================================================== + +start_recorder() { + log_step "Starting axon_recorder with mock middleware..." + + # Create test config + local config_file="${TEST_DIR}/test_config.yaml" + + cat > "${config_file}" < "${TEST_DIR}/recorder.log" 2>&1 & + RECORDER_PID=$! + + # Wait for recorder to start + sleep 3 + + # Check if recorder is running + if ! kill -0 "${RECORDER_PID}" 2>/dev/null; then + log_error "Failed to start recorder" + log_error "Log output:" + cat "${TEST_DIR}/recorder.log" + exit 1 + fi + + log_info "Recorder started (PID: ${RECORDER_PID})" + + # Show initial log output + log_info "Initial recorder log:" + head -n 20 "${TEST_DIR}/recorder.log" || true +} + +test_health_check() { + log_step "Testing health check endpoint..." + + local response + response=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:${HTTP_PORT}/health" || echo "000") + + if [[ "${response}" == "200" ]]; then + log_info "✓ Health check passed" + return 0 + else + log_error "✗ Health check failed (HTTP ${response})" + return 1 + fi +} + +test_cache_config() { + log_step "Caching task configuration..." + + local response + response=$(curl -s -X POST \ + -H "Content-Type: application/json" \ + -d "{ + \"task_config\": { + \"task_id\": \"${TEST_TASK_ID}\", + \"device_id\": \"${TEST_DEVICE_ID}\", + \"scene\": \"${TEST_SCENE}\", + \"factory\": \"mock_factory\", + \"operator_name\": \"mock_operator\", + \"topics\": [\"/mock/string\", \"/mock/int\", \"/mock/image\"] + } + }" \ + "http://localhost:${HTTP_PORT}/rpc/config") + + if echo "${response}" | grep -q '"success":true'; then + log_info "✓ Config cached successfully" + return 0 + else + log_error "✗ Config cache failed" + log_error "Response: ${response}" + return 1 + fi +} + +test_start_recording() { + log_step "Starting recording..." + + local response + response=$(curl -s -X POST \ + -H "Content-Type: application/json" \ + -d "{\"task_id\": \"${TEST_TASK_ID}\"}" \ + "http://localhost:${HTTP_PORT}/rpc/begin") + + if echo "${response}" | grep -q '"success":true'; then + log_info "✓ Recording started" + return 0 + else + log_error "✗ Failed to start recording" + log_error "Response: ${response}" + return 1 + fi +} + +test_wait_for_messages() { + log_step "Waiting for mock messages (3 seconds)..." + sleep 3 + log_info "Mock messages should have been published" +} + +test_stop_recording() { + log_step "Stopping recording..." + + local response + response=$(curl -s -X POST \ + -H "Content-Type: application/json" \ + -d "{\"task_id\": \"${TEST_TASK_ID}\"}" \ + "http://localhost:${HTTP_PORT}/rpc/finish") + + if echo "${response}" | grep -q '"success":true'; then + log_info "✓ Recording stopped" + return 0 + else + log_error "✗ Failed to stop recording" + log_error "Response: ${response}" + return 1 + fi +} + +verify_mcap_file() { + log_step "Verifying MCAP file..." + + # Find the most recent MCAP file + local mcap_file + mcap_file=$(find "${TEST_DIR}" -name "*.mcap" -type f 2>/dev/null | head -n 1) + + if [[ -z "${mcap_file}" ]]; then + log_error "✗ No MCAP file found" + return 1 + fi + + if [[ ! -s "${mcap_file}" ]]; then + log_error "✗ MCAP file is empty" + return 1 + fi + + local file_size + file_size=$(stat -f%z "${mcap_file}" 2>/dev/null || stat -c%s "${mcap_file}" 2>/dev/null) + + log_info "✓ MCAP file verified: ${mcap_file}" + log_info " Size: ${file_size} bytes" + + # Try to get basic info with mcap (if available) + if command -v mcap &> /dev/null; then + log_info " MCAP info:" + mcap info "${mcap_file}" 2>&1 | head -n 10 || true + fi + + return 0 +} + +verify_sidecar_file() { + log_step "Verifying sidecar JSON file..." + + local mcap_file + mcap_file=$(find "${TEST_DIR}" -name "*.mcap" -type f 2>/dev/null | head -n 1) + + if [[ -z "${mcap_file}" ]]; then + log_error "Cannot verify sidecar without MCAP file" + return 1 + fi + + local sidecar_file="${mcap_file%.mcap}.json" + + if [[ ! -f "${sidecar_file}" ]]; then + log_warn "Sidecar file not found: ${sidecar_file}" + return 0 + fi + + # Verify JSON is valid + if ! python3 -m json.tool "${sidecar_file}" > /dev/null 2>&1; then + log_error "✗ Sidecar file is not valid JSON" + return 1 + fi + + log_info "✓ Sidecar file verified: ${sidecar_file}" + + # Show some key fields + log_info " Task ID: $(grep '"task_id"' "${sidecar_file}" | cut -d'"' -f4)" + log_info " Device ID: $(grep '"device_id"' "${sidecar_file}" | cut -d'"' -f4)" + + return 0 +} + +show_recorder_stats() { + log_step "Showing recorder statistics..." + + if [[ -f "${TEST_DIR}/stats.json" ]]; then + log_info "Recorder statistics:" + cat "${TEST_DIR}/stats.json" + else + log_warn "No statistics file found" + fi +} + +# ============================================================================== +# Main Test Runner +# ============================================================================== + +run_all_tests() { + local test_count=0 + local passed_count=0 + local failed_count=0 + + log_info "================================" + log_info "Mock Middleware E2E Tests" + log_info "================================" + + # Array of test functions + local tests=( + "test_health_check" + "test_cache_config" + "test_start_recording" + "test_wait_for_messages" + "test_stop_recording" + "verify_mcap_file" + "verify_sidecar_file" + ) + + # Run each test + for test_func in "${tests[@]}"; do + test_count=$((test_count + 1)) + + if ${test_func}; then + passed_count=$((passed_count + 1)) + else + failed_count=$((failed_count + 1)) + fi + done + + # Show statistics + show_recorder_stats + + # Print summary + log_info "================================" + log_info "Test Summary" + log_info "================================" + log_info "Total: ${test_count}" + log_info "Passed: ${passed_count}" + log_info "Failed: ${failed_count}" + log_info "================================" + + if [[ ${failed_count} -eq 0 ]]; then + log_info "All tests passed!" + return 0 + else + log_error "Some tests failed!" + return 1 + fi +} + +# ============================================================================== +# Main +# ============================================================================== + +main() { + log_info "================================" + log_info "Mock Middleware E2E Test" + log_info "================================" + log_info "" + + setup + start_recorder + run_all_tests +} + +# Allow keeping test data for inspection +KEEP_TEST_DATA=${KEEP_TEST_DATA:-0} + +main "$@" diff --git a/middlewares/mock/test_full_workflow.sh b/middlewares/mock/test_full_workflow.sh new file mode 100644 index 0000000..94d6c68 --- /dev/null +++ b/middlewares/mock/test_full_workflow.sh @@ -0,0 +1,66 @@ +#!/bin/bash +# @file test_full_workflow.sh +# @brief Full workflow test for mock middleware (without axon_recorder) +# +# This script demonstrates the complete mock middleware functionality: +# 1. Build mock middleware +# 2. Test plugin loading via PluginLoader +# 3. Test direct E2E functionality +# 4. Show message publishing and subscription + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" + +# Colors +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_step() { + echo -e "${BLUE}[STEP]${NC} $1" +} + +log_section() { + echo -e "\n${YELLOW}================================${NC}" + echo -e "${YELLOW}$1${NC}" + echo -e "${YELLOW}================================${NC}" +} + +# ============================================================================== +# Main +# ============================================================================== + +main() { + log_section "Mock Middleware Full Workflow Test" + + log_step "1. Building mock middleware..." + cd "${PROJECT_ROOT}" + make build-mock + + log_step "2. Running plugin load test (tests C ABI interface)..." + make test-mock-load + + log_step "3. Running E2E test (tests direct plugin functionality)..." + make test-mock-e2e + + log_section "All Tests Completed Successfully!" + + log_info "Summary:" + log_info " ✓ Mock middleware built" + log_info " ✓ Plugin C ABI interface verified" + log_info " ✓ E2E functionality verified" + log_info "" + log_info "The mock middleware is ready for:" + log_info " - Integration testing with axon_recorder" + log_info " - CI/CD pipelines (no ROS dependencies)" + log_info " - Plugin development reference" +} + +main "$@" From 17be492b828bba0c6d50d12c2cdb35a5cb626587 Mon Sep 17 00:00:00 2001 From: Liangwei XU Date: Wed, 21 Jan 2026 16:23:18 +0800 Subject: [PATCH 02/10] feat: add frontend of control panel --- apps/axon_recorder/http_server.cpp | 19 + docs/designs/frontend-design.md | 574 ++++++ docs/designs/rpc-api-design.md | 170 +- tools/axon_panel/.gitignore | 7 + tools/axon_panel/README.md | 105 + tools/axon_panel/index.html | 35 + tools/axon_panel/package-lock.json | 1729 +++++++++++++++++ tools/axon_panel/package.json | 21 + tools/axon_panel/run.sh | 19 + tools/axon_panel/src/App.vue | 502 +++++ tools/axon_panel/src/api/rpc.js | 114 ++ .../axon_panel/src/components/ConfigPanel.vue | 391 ++++ .../src/components/ConnectionStatus.vue | 124 ++ .../src/components/ControlPanel.vue | 231 +++ tools/axon_panel/src/components/LogPanel.vue | 85 + .../src/components/StateMachineDiagram.vue | 485 +++++ .../axon_panel/src/components/StatePanel.vue | 251 +++ tools/axon_panel/src/edge-label-fix.css | 103 + tools/axon_panel/src/main.js | 5 + tools/axon_panel/vite.config.js | 20 + 20 files changed, 4908 insertions(+), 82 deletions(-) create mode 100644 docs/designs/frontend-design.md create mode 100644 tools/axon_panel/.gitignore create mode 100644 tools/axon_panel/README.md create mode 100644 tools/axon_panel/index.html create mode 100644 tools/axon_panel/package-lock.json create mode 100644 tools/axon_panel/package.json create mode 100644 tools/axon_panel/run.sh create mode 100644 tools/axon_panel/src/App.vue create mode 100644 tools/axon_panel/src/api/rpc.js create mode 100644 tools/axon_panel/src/components/ConfigPanel.vue create mode 100644 tools/axon_panel/src/components/ConnectionStatus.vue create mode 100644 tools/axon_panel/src/components/ControlPanel.vue create mode 100644 tools/axon_panel/src/components/LogPanel.vue create mode 100644 tools/axon_panel/src/components/StateMachineDiagram.vue create mode 100644 tools/axon_panel/src/components/StatePanel.vue create mode 100644 tools/axon_panel/src/edge-label-fix.css create mode 100644 tools/axon_panel/src/main.js create mode 100644 tools/axon_panel/vite.config.js diff --git a/apps/axon_recorder/http_server.cpp b/apps/axon_recorder/http_server.cpp index fa2832d..ccdc12b 100644 --- a/apps/axon_recorder/http_server.cpp +++ b/apps/axon_recorder/http_server.cpp @@ -545,6 +545,25 @@ HttpServer::RpcResponse HttpServer::handle_rpc_get_state(const nlohmann::json& p response.data["state"] = "unknown"; } + // Get task config if available (READY, RECORDING, PAUSED states) + if (callbacks_.get_task_config) { + const TaskConfig* task_config = callbacks_.get_task_config(); + if (task_config) { + nlohmann::json config_json; + config_json["task_id"] = task_config->task_id; + config_json["device_id"] = task_config->device_id; + config_json["data_collector_id"] = task_config->data_collector_id; + config_json["scene"] = task_config->scene; + config_json["subscene"] = task_config->subscene; + config_json["skills"] = task_config->skills; + config_json["factory"] = task_config->factory; + config_json["operator_name"] = task_config->operator_name; + config_json["topics"] = task_config->topics; + // Note: Don't include sensitive fields like callback URLs and tokens + response.data["task_config"] = config_json; + } + } + // Get stats from callback to check if running if (callbacks_.get_stats) { try { diff --git a/docs/designs/frontend-design.md b/docs/designs/frontend-design.md new file mode 100644 index 0000000..44760e5 --- /dev/null +++ b/docs/designs/frontend-design.md @@ -0,0 +1,574 @@ +# AxonPanel - Frontend Design Document + +**Date:** 2025-01-21 +**Status:** Implemented + +## Overview + +AxonPanel is a modern Vue 3-based web interface for debugging and controlling the Axon Recorder HTTP RPC API. It provides real-time monitoring, recording control, and visual state machine representation. + + +## Features + +- **Real-time State Monitoring**: View current recorder state and task configuration +- **Recording Statistics**: Monitor messages received, written, dropped, and file size +- **Control Panel**: Execute RPC commands (config, begin, pause, resume, finish, cancel) +- **Activity Log**: Track all RPC interactions with timestamps and color-coded messages +- **Visual State Machine**: Interactive diagram showing state transitions +- **Responsive Design**: Mobile-friendly interface with touch-optimized controls +- **Auto-refresh**: Polls state and statistics every second + +## Architecture + +### Component Structure + +``` +App.vue (Root Component) +├── ConnectionStatus.vue - Connection status and health check +├── StatePanel.vue - Statistics and task configuration display +├── ControlPanel.vue - Command buttons and state machine diagram +│ └── StateMachineDiagram.vue - Visual state transition diagram +├── ConfigPanel.vue - Modal form for task configuration +└── LogPanel.vue - Activity log display +``` + +### Data Flow + +``` +User Action → ControlPanel + ↓ +App.vue (Command Handler) + ↓ +RPC API Client (api/rpc.js) + ↓ +Axon Recorder (HTTP Server) + ↓ +Response → App.vue (State Update) + ↓ +Component Re-render (Vue Reactivity) +``` + +### Directory Structure + +``` +tools/axon_panel/ +├── src/ +│ ├── api/ +│ │ └── rpc.js # Centralized API client +│ ├── components/ +│ │ ├── ConnectionStatus.vue +│ │ ├── StatePanel.vue +│ │ ├── ControlPanel.vue +│ │ ├── StateMachineDiagram.vue +│ │ ├── ConfigPanel.vue +│ │ └── LogPanel.vue +│ ├── App.vue # Root component +│ ├── main.js # Entry point +│ └── edge-label-fix.css # Vue Flow edge label fix +├── index.html # HTML template +├── package.json # Dependencies +├── vite.config.js # Vite configuration +└── README.md # User documentation +``` + +## Key Components + +### 1. App.vue - Main Application Container + +**File:** [tools/axon_panel/src/App.vue](../../tools/axon_panel/src/App.vue) + +**Responsibilities:** +- Manages global state (current state, task config, statistics, logs) +- Handles RPC command execution and error handling +- Polls server for state updates (1-second interval) +- Manages activity log with FIFO (100 entries max) + +**State Management:** +```javascript +const connected = ref(false) // Server connection status +const health = ref(null) // Health check data +const currentState = ref('idle') // Current recorder state +const taskConfig = ref(null) // Cached task configuration +const currentTaskId = ref('') // Active task ID +const stats = ref(null) // Recording statistics +const showConfigPanel = ref(false) // Config modal visibility +const showLogs = ref(true) // Log panel visibility +const logs = ref([]) // Activity log entries +``` + +**Polling Strategy:** +- Automatic state refresh every 1000ms +- Starts on component mount +- Stops on component unmount +- Fetches both state and stats + +**Command Flow:** +1. User clicks button → Emit command event +2. `handleCommand()` executes appropriate RPC call +3. Success → Log message + refresh state +4. Error → Log error message + +### 2. ConnectionStatus.vue - Connection Indicator + +**File:** [tools/axon_panel/src/components/ConnectionStatus.vue](../../tools/axon_panel/src/components/ConnectionStatus.vue) + +**Features:** +- Visual connection status badge (Connected/Disconnected) +- Display recorder version +- Manual refresh button + +**Health Check:** +- Endpoint: `GET /health` +- Updates: On mount + manual refresh +- Error handling: Graceful degradation + +**Props:** +```javascript +{ + connected: Boolean, // Connection status + health: Object // Health data { version, running, state } +} +``` + +**Emits:** +- `refresh` - Manual health check request + +### 3. StatePanel.vue - Statistics Display + +**File:** [tools/axon_panel/src/components/StatePanel.vue](../../tools/axon_panel/src/components/StatePanel.vue) + +**Displays:** +- Task configuration: + - Task ID, Device ID, Scene, Factory + - Topics (as tags) +- Recording statistics: + - Messages received (formatted number) + - Messages written (formatted number) + - Messages dropped (formatted number) + - File size (formatted bytes) + +**Formatting Functions:** +```javascript +formatNumber(1523456) // "1,523,456" +formatBytes(2147483648) // "2.00 GB" +``` + +**Update Frequency:** +- Syncs with parent's 1-second polling +- Manual refresh button + +### 4. ControlPanel.vue - Command Center + +**File:** [tools/axon_panel/src/components/ControlPanel.vue](../../tools/axon_panel/src/components/ControlPanel.vue) + +**Features:** +- Command buttons with state-based enable/disable +- Current task ID display +- Embedded state machine diagram + +**Button Logic:** +```javascript +config: enabled when state !== 'recording' && state !== 'paused' +begin: enabled when state === 'ready' +pause: enabled when state === 'recording' +resume: enabled when state === 'paused' +finish: enabled when state === 'recording' || state === 'paused' +cancel: enabled when state === 'recording' || state === 'paused' +clear: enabled when state === 'ready' +quit: always enabled +``` + +**Emits:** +- `command` - With command name as payload + +### 5. StateMachineDiagram.vue - Visual State Machine + +**File:** [tools/axon_panel/src/components/StateMachineDiagram.vue](../../tools/axon_panel/src/components/StateMachineDiagram.vue) + +**Technology:** Vue Flow (flow chart library) + +**Features:** +- Four-state visualization (IDLE, READY, RECORDING, PAUSED) +- Dynamic edge visibility based on current state +- Active state highlighting with color coding +- Smooth transitions between states + +**State Colors:** +``` +IDLE: Gray (#9ca3af) +READY: Green (#10b981) +RECORDING: Red (#ef4444) +PAUSED: Yellow (#f59e0b) +``` + +**Edge Visibility Logic:** +```javascript +idle: [idle→ready] +ready: [ready→recording, ready→idle] +recording: [recording→paused, recording→idle] +paused: [paused→recording, paused→idle] +``` + +**Special Implementation:** +- Forces edge labels to black (CSS workaround for Vue Flow) +- Disables panning, zooming, and node dragging +- Responsive sizing (350px height) + +### 6. ConfigPanel.vue - Task Configuration Modal + +**File:** [tools/axon_panel/src/components/ConfigPanel.vue](../../tools/axon_panel/src/components/ConfigPanel.vue) + +**Features:** +- Modal dialog with Teleport to body +- Form validation (HTML5 required attributes) +- Pre-filled default values for quick testing +- Comma-separated array inputs (skills, topics) +- Only shown when state !== 'recording' && state !== 'paused' + +**Form Fields:** +```javascript +{ + task_id: string, // Required + device_id: string, // Required + data_collector_id: string, // Optional + scene: string, // Optional + subscene: string, // Optional + factory: string, // Optional + operator_name: string, // Optional + skills: string[], // Comma-separated input + topics: string[], // Comma-separated input + start_callback_url: string, // Optional (URL validation) + finish_callback_url: string, // Optional (URL validation) + user_token: string // Optional (textarea) +} +``` + +**Default Values:** +```javascript +task_id: `task_${Date.now()}` +device_id: 'robot_01' +data_collector_id: 'collector_01' +scene: 'warehouse_navigation' +subscene: 'aisle_traversal' +factory: 'factory_shenzhen' +operator_name: 'operator_001' +skills: ['navigation', 'obstacle_avoidance'] +topics: ['/camera/image', '/lidar/scan', '/odom'] +``` + +**Emits:** +- `submit` - With form data object +- `cancel` - Modal closed without submission + +### 7. LogPanel.vue - Activity Logger + +**File:** [tools/axon_panel/src/components/LogPanel.vue](../../tools/axon_panel/src/components/LogPanel.vue) + +**Features:** +- Chronological log with timestamps +- Color-coded message types +- Terminal-like appearance (dark background) +- Empty state message + +**Message Types:** +```javascript +{ + timestamp: string, // HH:MM:SS format + message: string, // Log message + type: string // 'info' | 'success' | 'warning' | 'error' +} +``` + +**Color Scheme:** +```javascript +info: Blue (#60a5fa) +success: Green (#34d399) +warning: Yellow (#fbbf24) +error: Red (#f87171) +``` + +**Props:** +```javascript +{ + logs: Array // Array of log entry objects +} +``` + +## RPC API Integration + +**File:** [tools/axon_panel/src/api/rpc.js](../../tools/axon_panel/src/api/rpc.js) + +The centralized API client provides methods for all RPC endpoints: + +```javascript +// Query endpoints +rpcApi.health() // GET /health +rpcApi.getState() // GET /rpc/state +rpcApi.getStats() // GET /rpc/stats + +// Command endpoints +rpcApi.setConfig(config) // POST /rpc/config +rpcApi.begin(taskId) // POST /rpc/begin +rpcApi.finish(taskId) // POST /rpc/finish +rpcApi.pause() // POST /rpc/pause +rpcApi.resume() // POST /rpc/resume +rpcApi.cancel(taskId) // POST /rpc/cancel +rpcApi.clear() // POST /rpc/clear +rpcApi.quit() // POST /rpc/quit +``` + +**Base URL Configuration:** +```javascript +const API_BASE_URL = 'http://localhost:8080' +``` + +**Response Format:** +All methods return promises that resolve to the response JSON: +```javascript +{ + success: boolean, + message: string, + data: { + state: string, + task_id: string, + // ... other fields + } +} +``` + +See [RPC API Design](rpc-api-design.md) for detailed API documentation. + +## State Machine + +The application follows the Axon Recorder state machine: + +``` +IDLE ──(POST /rpc/config)──► READY ──(POST /rpc/begin)──► RECORDING ↔ PAUSED + ▲ │ │ + │ │ │ + └────────(POST /rpc/finish)───┴──────────────(POST /rpc/finish)──────┘ + │ + ▼ + (POST /rpc/quit) + │ + ▼ + Program Exit +``` + +### State Transitions + +| Current State | Valid Commands | Result State | +|---------------|----------------|--------------| +| `IDLE` | config | `READY` | +| `READY` | begin | `RECORDING` | +| `READY` | clear | `IDLE` | +| `RECORDING` | pause | `PAUSED` | +| `RECORDING` | finish | `IDLE` | +| `RECORDING` | cancel | `IDLE` | +| `PAUSED` | resume | `RECORDING` | +| `PAUSED` | finish | `IDLE` | +| `PAUSED` | cancel | `IDLE` | +| Any State | quit | Program Exit | + +## UI/UX Design + +### Design Principles + +1. **Mobile-First**: Responsive layout optimized for tablets and phones +2. **Touch-Friendly**: Minimum 44px tap targets for buttons +3. **Visual Feedback**: Color-coded states and animated transitions +4. **Real-Time Updates**: Auto-refreshing data without manual reload +5. **Error Handling**: User-friendly error messages in activity log + +### Color Scheme + +``` +Primary: Indigo (#4f46e5) - Header, active elements +Success: Green (#10b981) - Ready state, success messages +Danger: Red (#ef4444) - Recording state, errors, cancel action +Warning: Yellow (#f59e0b) - Paused state, warnings +Neutral: Gray (#6b7280) - Idle state, labels +``` + +### Responsive Breakpoints + +**Desktop (> 768px):** +- Multi-column grid layout (auto-fit, minmax(350px, 1fr)) +- Floating log panel (600px × 500px, fixed position) +- Full-size buttons and text +- Hover states for buttons + +**Mobile (≤ 768px):** +- Single-column layout +- Full-width log panel (60vh height, fixed to bottom) +- Touch-optimized buttons (min 44px × 44px) +- Prevented pull-to-refresh: `overscroll-behavior-y: contain` +- Prevented zoom on input focus: `font-size: 16px` + +### Animations + +**Modal Transitions:** +```css +.modal-enter-active, .modal-leave-active { + transition: all 0.3s ease; +} +.modal-enter-from, .modal-leave-to { + opacity: 0; +} +.modal-enter-from .modal-content, .modal-leave-to .modal-content { + transform: scale(0.95); +} +``` + +**Slide Transitions (Log Panel):** +```css +.slide-enter-active, .slide-leave-active { + transition: all 0.3s ease; +} +.slide-enter-from, .slide-leave-to { + opacity: 0; + transform: translateY(20px) scale(0.95); +} +``` + +## Technology Stack + +- **Vue 3** (v3.4+): Progressive JavaScript framework with Composition API +- **Vite** (v5.0+): Build tool and dev server with HMR +- **Vue Flow** (v1.48+): Interactive flow chart library for state machine visualization +- **Axios** (v1.6+): HTTP client for API requests + +### Dependencies + +```json +{ + "dependencies": { + "@vue-flow/background": "^1.3.2", + "@vue-flow/controls": "^1.1.3", + "@vue-flow/core": "^1.48.1", + "axios": "^1.6.0", + "vue": "^3.4.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.0", + "vite": "^5.0.0" + } +} +``` + +## Development + +### File Naming Convention +- Components: PascalCase (e.g., `ConnectionStatus.vue`) +- Utilities: camelCase (e.g., `rpc.js`) + +### Code Style +- **Vue 3 Composition API**: Uses ` + + diff --git a/tools/axon_panel/package-lock.json b/tools/axon_panel/package-lock.json new file mode 100644 index 0000000..c515e54 --- /dev/null +++ b/tools/axon_panel/package-lock.json @@ -0,0 +1,1729 @@ +{ + "name": "axon-panel", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "axon-panel", + "version": "0.1.0", + "dependencies": { + "@vue-flow/background": "^1.3.2", + "@vue-flow/controls": "^1.1.3", + "@vue-flow/core": "^1.48.1", + "axios": "^1.6.0", + "vue": "^3.4.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.0", + "vite": "^5.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.2.tgz", + "integrity": "sha512-21J6xzayjy3O6NdnlO6aXi/urvSRjm6nCI6+nF6ra2YofKruGixN9kfT+dt55HVNwfDmpDHJcaS3JuP/boNnlA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.2.tgz", + "integrity": "sha512-eXBg7ibkNUZ+sTwbFiDKou0BAckeV6kIigK7y5Ko4mB/5A1KLhuzEKovsmfvsL8mQorkoincMFGnQuIT92SKqA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.2.tgz", + "integrity": "sha512-UCbaTklREjrc5U47ypLulAgg4njaqfOVLU18VrCrI+6E5MQjuG0lSWaqLlAJwsD7NpFV249XgB0Bi37Zh5Sz4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.2.tgz", + "integrity": "sha512-dP67MA0cCMHFT2g5XyjtpVOtp7y4UyUxN3dhLdt11at5cPKnSm4lY+EhwNvDXIMzAMIo2KU+mc9wxaAQJTn7sQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.2.tgz", + "integrity": "sha512-WDUPLUwfYV9G1yxNRJdXcvISW15mpvod1Wv3ok+Ws93w1HjIVmCIFxsG2DquO+3usMNCpJQ0wqO+3GhFdl6Fow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.2.tgz", + "integrity": "sha512-Ng95wtHVEulRwn7R0tMrlUuiLVL/HXA8Lt/MYVpy88+s5ikpntzZba1qEulTuPnPIZuOPcW9wNEiqvZxZmgmqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.2.tgz", + "integrity": "sha512-AEXMESUDWWGqD6LwO/HkqCZgUE1VCJ1OhbvYGsfqX2Y6w5quSXuyoy/Fg3nRqiwro+cJYFxiw5v4kB2ZDLhxrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.2.tgz", + "integrity": "sha512-ZV7EljjBDwBBBSv570VWj0hiNTdHt9uGznDtznBB4Caj3ch5rgD4I2K1GQrtbvJ/QiB+663lLgOdcADMNVC29Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.2.tgz", + "integrity": "sha512-uvjwc8NtQVPAJtq4Tt7Q49FOodjfbf6NpqXyW/rjXoV+iZ3EJAHLNAnKT5UJBc6ffQVgmXTUL2ifYiLABlGFqA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.2.tgz", + "integrity": "sha512-s3KoWVNnye9mm/2WpOZ3JeUiediUVw6AvY/H7jNA6qgKA2V2aM25lMkVarTDfiicn/DLq3O0a81jncXszoyCFA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.2.tgz", + "integrity": "sha512-gi21faacK+J8aVSyAUptML9VQN26JRxe484IbF+h3hpG+sNVoMXPduhREz2CcYr5my0NE3MjVvQ5bMKX71pfVA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.2.tgz", + "integrity": "sha512-qSlWiXnVaS/ceqXNfnoFZh4IiCA0EwvCivivTGbEu1qv2o+WTHpn1zNmCTAoOG5QaVr2/yhCoLScQtc/7RxshA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.2.tgz", + "integrity": "sha512-rPyuLFNoF1B0+wolH277E780NUKf+KoEDb3OyoLbAO18BbeKi++YN6gC/zuJoPPDlQRL3fIxHxCxVEWiem2yXw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.2.tgz", + "integrity": "sha512-g+0ZLMook31iWV4PvqKU0i9E78gaZgYpSrYPed/4Bu+nGTgfOPtfs1h11tSSRPXSjC5EzLTjV/1A7L2Vr8pJoQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.2.tgz", + "integrity": "sha512-i+sGeRGsjKZcQRh3BRfpLsM3LX3bi4AoEVqmGDyc50L6KfYsN45wVCSz70iQMwPWr3E5opSiLOwsC9WB4/1pqg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.2.tgz", + "integrity": "sha512-C1vLcKc4MfFV6I0aWsC7B2Y9QcsiEcvKkfxprwkPfLaN8hQf0/fKHwSF2lcYzA9g4imqnhic729VB9Fo70HO3Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.2.tgz", + "integrity": "sha512-68gHUK/howpQjh7g7hlD9DvTTt4sNLp1Bb+Yzw2Ki0xvscm2cOdCLZNJNhd2jW8lsTPrHAHuF751BygifW4bkQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.2.tgz", + "integrity": "sha512-1e30XAuaBP1MAizaOBApsgeGZge2/Byd6wV4a8oa6jPdHELbRHBiw7wvo4dp7Ie2PE8TZT4pj9RLGZv9N4qwlw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.2.tgz", + "integrity": "sha512-4BJucJBGbuGnH6q7kpPqGJGzZnYrpAzRd60HQSt3OpX/6/YVgSsJnNzR8Ot74io50SeVT4CtCWe/RYIAymFPwA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.2.tgz", + "integrity": "sha512-cT2MmXySMo58ENv8p6/O6wI/h/gLnD3D6JoajwXFZH6X9jz4hARqUhWpGuQhOgLNXscfZYRQMJvZDtWNzMAIDw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.2.tgz", + "integrity": "sha512-sZnyUgGkuzIXaK3jNMPmUIyJrxu/PjmATQrocpGA1WbCPX8H5tfGgRSuYtqBYAvLuIGp8SPRb1O4d1Fkb5fXaQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.2.tgz", + "integrity": "sha512-sDpFbenhmWjNcEbBcoTV0PWvW5rPJFvu+P7XoTY0YLGRupgLbFY0XPfwIbJOObzO7QgkRDANh65RjhPmgSaAjQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.2.tgz", + "integrity": "sha512-GvJ03TqqaweWCigtKQVBErw2bEhu1tyfNQbarwr94wCGnczA9HF8wqEe3U/Lfu6EdeNP0p6R+APeHVwEqVxpUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.2.tgz", + "integrity": "sha512-KvXsBvp13oZz9JGe5NYS7FNizLe99Ny+W8ETsuCyjXiKdiGrcz2/J/N8qxZ/RSwivqjQguug07NLHqrIHrqfYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.2.tgz", + "integrity": "sha512-xNO+fksQhsAckRtDSPWaMeT1uIM+JrDRXlerpnWNXhn1TdB3YZ6uKBMBTKP0eX9XtYEP978hHk1f8332i2AW8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue-flow/background": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@vue-flow/background/-/background-1.3.2.tgz", + "integrity": "sha512-eJPhDcLj1wEo45bBoqTXw1uhl0yK2RaQGnEINqvvBsAFKh/camHJd5NPmOdS1w+M9lggc9igUewxaEd3iCQX2w==", + "license": "MIT", + "peerDependencies": { + "@vue-flow/core": "^1.23.0", + "vue": "^3.3.0" + } + }, + "node_modules/@vue-flow/controls": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@vue-flow/controls/-/controls-1.1.3.tgz", + "integrity": "sha512-XCf+G+jCvaWURdFlZmOjifZGw3XMhN5hHlfMGkWh9xot+9nH9gdTZtn+ldIJKtarg3B21iyHU8JjKDhYcB6JMw==", + "license": "MIT", + "peerDependencies": { + "@vue-flow/core": "^1.23.0", + "vue": "^3.3.0" + } + }, + "node_modules/@vue-flow/core": { + "version": "1.48.1", + "resolved": "https://registry.npmjs.org/@vue-flow/core/-/core-1.48.1.tgz", + "integrity": "sha512-3IxaMBLvWRbznZ4CuK0kVhp4Y4lCDQx9nhi48Swp6PwPw29KNhmiKd2kaBogYeWjGLb/tLjlE9V0s3jEmKCYWw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vueuse/core": "^10.5.0", + "d3-drag": "^3.0.0", + "d3-interpolate": "^3.0.1", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + }, + "peerDependencies": { + "vue": "^3.3.0" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.27.tgz", + "integrity": "sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/shared": "3.5.27", + "entities": "^7.0.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.27.tgz", + "integrity": "sha512-oAFea8dZgCtVVVTEC7fv3T5CbZW9BxpFzGGxC79xakTr6ooeEqmRuvQydIiDAkglZEAd09LgVf1RoDnL54fu5w==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.27", + "@vue/shared": "3.5.27" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.27.tgz", + "integrity": "sha512-sHZu9QyDPeDmN/MRoshhggVOWE5WlGFStKFwu8G52swATgSny27hJRWteKDSUUzUH+wp+bmeNbhJnEAel/auUQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/compiler-core": "3.5.27", + "@vue/compiler-dom": "3.5.27", + "@vue/compiler-ssr": "3.5.27", + "@vue/shared": "3.5.27", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.27.tgz", + "integrity": "sha512-Sj7h+JHt512fV1cTxKlYhg7qxBvack+BGncSpH+8vnN+KN95iPIcqB5rsbblX40XorP+ilO7VIKlkuu3Xq2vjw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.27", + "@vue/shared": "3.5.27" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.27.tgz", + "integrity": "sha512-vvorxn2KXfJ0nBEnj4GYshSgsyMNFnIQah/wczXlsNXt+ijhugmW+PpJ2cNPe4V6jpnBcs0MhCODKllWG+nvoQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.27" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.27.tgz", + "integrity": "sha512-fxVuX/fzgzeMPn/CLQecWeDIFNt3gQVhxM0rW02Tvp/YmZfXQgcTXlakq7IMutuZ/+Ogbn+K0oct9J3JZfyk3A==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.27", + "@vue/shared": "3.5.27" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.27.tgz", + "integrity": "sha512-/QnLslQgYqSJ5aUmb5F0z0caZPGHRB8LEAQ1s81vHFM5CBfnun63rxhvE/scVb/j3TbBuoZwkJyiLCkBluMpeg==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.27", + "@vue/runtime-core": "3.5.27", + "@vue/shared": "3.5.27", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.27.tgz", + "integrity": "sha512-qOz/5thjeP1vAFc4+BY3Nr6wxyLhpeQgAE/8dDtKo6a6xdk+L4W46HDZgNmLOBUDEkFXV3G7pRiUqxjX0/2zWA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.27", + "@vue/shared": "3.5.27" + }, + "peerDependencies": { + "vue": "3.5.27" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.27.tgz", + "integrity": "sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==", + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz", + "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "10.11.1", + "@vueuse/shared": "10.11.1", + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/core/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz", + "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz", + "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==", + "license": "MIT", + "dependencies": { + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.0.tgz", + "integrity": "sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.2.tgz", + "integrity": "sha512-PggGy4dhwx5qaW+CKBilA/98Ql9keyfnb7lh4SR6shQ91QQQi1ORJ1v4UinkdP2i87OBs9AQFooQylcrrRfIcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.55.2", + "@rollup/rollup-android-arm64": "4.55.2", + "@rollup/rollup-darwin-arm64": "4.55.2", + "@rollup/rollup-darwin-x64": "4.55.2", + "@rollup/rollup-freebsd-arm64": "4.55.2", + "@rollup/rollup-freebsd-x64": "4.55.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.2", + "@rollup/rollup-linux-arm-musleabihf": "4.55.2", + "@rollup/rollup-linux-arm64-gnu": "4.55.2", + "@rollup/rollup-linux-arm64-musl": "4.55.2", + "@rollup/rollup-linux-loong64-gnu": "4.55.2", + "@rollup/rollup-linux-loong64-musl": "4.55.2", + "@rollup/rollup-linux-ppc64-gnu": "4.55.2", + "@rollup/rollup-linux-ppc64-musl": "4.55.2", + "@rollup/rollup-linux-riscv64-gnu": "4.55.2", + "@rollup/rollup-linux-riscv64-musl": "4.55.2", + "@rollup/rollup-linux-s390x-gnu": "4.55.2", + "@rollup/rollup-linux-x64-gnu": "4.55.2", + "@rollup/rollup-linux-x64-musl": "4.55.2", + "@rollup/rollup-openbsd-x64": "4.55.2", + "@rollup/rollup-openharmony-arm64": "4.55.2", + "@rollup/rollup-win32-arm64-msvc": "4.55.2", + "@rollup/rollup-win32-ia32-msvc": "4.55.2", + "@rollup/rollup-win32-x64-gnu": "4.55.2", + "@rollup/rollup-win32-x64-msvc": "4.55.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.27.tgz", + "integrity": "sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.5.27", + "@vue/compiler-sfc": "3.5.27", + "@vue/runtime-dom": "3.5.27", + "@vue/server-renderer": "3.5.27", + "@vue/shared": "3.5.27" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + } + } +} diff --git a/tools/axon_panel/package.json b/tools/axon_panel/package.json new file mode 100644 index 0000000..73b5c8a --- /dev/null +++ b/tools/axon_panel/package.json @@ -0,0 +1,21 @@ +{ + "name": "axon-panel", + "version": "0.1.0", + "description": "Axon Recorder Web Control Panel", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@vue-flow/background": "^1.3.2", + "@vue-flow/controls": "^1.1.3", + "@vue-flow/core": "^1.48.1", + "axios": "^1.6.0", + "vue": "^3.4.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.0", + "vite": "^5.0.0" + } +} diff --git a/tools/axon_panel/run.sh b/tools/axon_panel/run.sh new file mode 100644 index 0000000..dce8897 --- /dev/null +++ b/tools/axon_panel/run.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Quick start script for Axon Webtool + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +# Check if node_modules exists +if [ ! -d "node_modules" ]; then + echo "Installing dependencies..." + npm install +fi + +echo "Starting Axon Webtool development server..." +echo "Open http://localhost:3000 in your browser" +echo "" +echo "Make sure Axon Recorder is running on port 8080" +echo "" + +npm run dev diff --git a/tools/axon_panel/src/App.vue b/tools/axon_panel/src/App.vue new file mode 100644 index 0000000..dd6af6c --- /dev/null +++ b/tools/axon_panel/src/App.vue @@ -0,0 +1,502 @@ + + + + + + + diff --git a/tools/axon_panel/src/api/rpc.js b/tools/axon_panel/src/api/rpc.js new file mode 100644 index 0000000..6183334 --- /dev/null +++ b/tools/axon_panel/src/api/rpc.js @@ -0,0 +1,114 @@ +import axios from 'axios' + +const BASE_URL = import.meta.env.VITE_API_BASE_URL || '' + +// Create axios instance with interceptors for debugging +const apiClient = axios.create({ + baseURL: BASE_URL, + headers: { + 'Content-Type': 'application/json' + } +}) + +// Request interceptor +apiClient.interceptors.request.use( + (config) => { + console.log('[API Request]', config.method.toUpperCase(), config.url, config.data) + return config + }, + (error) => { + console.error('[API Request Error]', error) + return Promise.reject(error) + } +) + +// Response interceptor +apiClient.interceptors.response.use( + (response) => { + console.log('[API Response]', response.config.url, response.data) + return response + }, + (error) => { + console.error('[API Response Error]', error.config?.url, error.message) + if (error.response) { + console.error('[API Error Response]', error.response.data) + } + return Promise.reject(error) + } +) + +export const rpcApi = { + // Health check + async health() { + const response = await apiClient.get('/health') + return response.data + }, + + // Get current state + async getState() { + const response = await apiClient.get('/rpc/state') + return response.data + }, + + // Get statistics + async getStats() { + const response = await apiClient.get('/rpc/stats') + return response.data + }, + + // Set task configuration + async setConfig(taskConfig) { + const response = await apiClient.post('/rpc/config', { + task_config: taskConfig + }) + return response.data + }, + + // Begin recording + async begin(taskId) { + const response = await apiClient.post('/rpc/begin', { + task_id: taskId + }) + return response.data + }, + + // Finish recording + async finish(taskId) { + const response = await apiClient.post('/rpc/finish', { + task_id: taskId + }) + return response.data + }, + + // Pause recording + async pause() { + const response = await apiClient.post('/rpc/pause') + return response.data + }, + + // Resume recording + async resume() { + const response = await apiClient.post('/rpc/resume') + return response.data + }, + + // Cancel recording + async cancel(taskId) { + const response = await apiClient.post('/rpc/cancel', { + task_id: taskId + }) + return response.data + }, + + // Clear configuration + async clear() { + const response = await apiClient.post('/rpc/clear') + return response.data + }, + + // Quit program + async quit() { + const response = await apiClient.post('/rpc/quit') + return response.data + } +} diff --git a/tools/axon_panel/src/components/ConfigPanel.vue b/tools/axon_panel/src/components/ConfigPanel.vue new file mode 100644 index 0000000..876f0d7 --- /dev/null +++ b/tools/axon_panel/src/components/ConfigPanel.vue @@ -0,0 +1,391 @@ + + + + + diff --git a/tools/axon_panel/src/components/ConnectionStatus.vue b/tools/axon_panel/src/components/ConnectionStatus.vue new file mode 100644 index 0000000..30b4af1 --- /dev/null +++ b/tools/axon_panel/src/components/ConnectionStatus.vue @@ -0,0 +1,124 @@ + + + + + diff --git a/tools/axon_panel/src/components/ControlPanel.vue b/tools/axon_panel/src/components/ControlPanel.vue new file mode 100644 index 0000000..72654f6 --- /dev/null +++ b/tools/axon_panel/src/components/ControlPanel.vue @@ -0,0 +1,231 @@ + + + + + diff --git a/tools/axon_panel/src/components/LogPanel.vue b/tools/axon_panel/src/components/LogPanel.vue new file mode 100644 index 0000000..bd2cac8 --- /dev/null +++ b/tools/axon_panel/src/components/LogPanel.vue @@ -0,0 +1,85 @@ + + + + + diff --git a/tools/axon_panel/src/components/StateMachineDiagram.vue b/tools/axon_panel/src/components/StateMachineDiagram.vue new file mode 100644 index 0000000..d2600c7 --- /dev/null +++ b/tools/axon_panel/src/components/StateMachineDiagram.vue @@ -0,0 +1,485 @@ + + + + + diff --git a/tools/axon_panel/src/components/StatePanel.vue b/tools/axon_panel/src/components/StatePanel.vue new file mode 100644 index 0000000..4e76299 --- /dev/null +++ b/tools/axon_panel/src/components/StatePanel.vue @@ -0,0 +1,251 @@ + + + + + diff --git a/tools/axon_panel/src/edge-label-fix.css b/tools/axon_panel/src/edge-label-fix.css new file mode 100644 index 0000000..1b17e25 --- /dev/null +++ b/tools/axon_panel/src/edge-label-fix.css @@ -0,0 +1,103 @@ +/* Global CSS for Vue Flow edge labels - force black color */ +/* This file is NOT scoped to ensure it applies to Vue Flow's dynamically generated elements */ + +/* Force all edge label text to be black */ +.vue-flow__edgeLabel text, +.vue-flow__edgeLabel tspan, +.vue-flow__edge-textwrapper text, +.vue-flow__edge-textwrapper tspan, +svg .vue-flow__edgeLabel text, +svg .vue-flow__edgeLabel tspan, +svg .vue-flow__edge-textwrapper text, +svg .vue-flow__edge-textwrapper tspan { + fill: #000000 !important; + color: #000000 !important; + stroke: none !important; +} + +/* Additional selectors for edge types */ +.vue-flow__edge.forward-edge .vue-flow__edgeLabel text, +.vue-flow__edge.forward-edge .vue-flow__edgeLabel tspan, +.vue-flow__edge.forward-edge .vue-flow__edge-textwrapper text, +.vue-flow__edge.forward-edge .vue-flow__edge-textwrapper tspan, +svg .vue-flow__edge.forward-edge .vue-flow__edgeLabel text, +svg .vue-flow__edge.forward-edge .vue-flow__edgeLabel tspan, +svg .vue-flow__edge.forward-edge .vue-flow__edge-textwrapper text, +svg .vue-flow__edge.forward-edge .vue-flow__edge-textwrapper tspan { + fill: #000000 !important; + color: #000000 !important; + stroke: none !important; +} + +.vue-flow__edge.bidirectional-edge .vue-flow__edgeLabel text, +.vue-flow__edge.bidirectional-edge .vue-flow__edgeLabel tspan, +.vue-flow__edge.bidirectional-edge .vue-flow__edge-textwrapper text, +.vue-flow__edge.bidirectional-edge .vue-flow__edge-textwrapper tspan, +svg .vue-flow__edge.bidirectional-edge .vue-flow__edgeLabel text, +svg .vue-flow__edge.bidirectional-edge .vue-flow__edgeLabel tspan, +svg .vue-flow__edge.bidirectional-edge .vue-flow__edge-textwrapper text, +svg .vue-flow__edge.bidirectional-edge .vue-flow__edge-textwrapper tspan { + fill: #000000 !important; + color: #000000 !important; + stroke: none !important; +} + +.vue-flow__edge.return-edge .vue-flow__edgeLabel text, +.vue-flow__edge.return-edge .vue-flow__edgeLabel tspan, +.vue-flow__edge.return-edge .vue-flow__edge-textwrapper text, +.vue-flow__edge.return-edge .vue-flow__edge-textwrapper tspan, +svg .vue-flow__edge.return-edge .vue-flow__edgeLabel text, +svg .vue-flow__edge.return-edge .vue-flow__edgeLabel tspan, +svg .vue-flow__edge.return-edge .vue-flow__edge-textwrapper text, +svg .vue-flow__edge.return-edge .vue-flow__edge-textwrapper tspan { + fill: #000000 !important; + color: #000000 !important; + stroke: none !important; +} + +.vue-flow__edge.cancel-edge .vue-flow__edgeLabel text, +.vue-flow__edge.cancel-edge .vue-flow__edgeLabel tspan, +.vue-flow__edge.cancel-edge .vue-flow__edge-textwrapper text, +.vue-flow__edge.cancel-edge .vue-flow__edge-textwrapper tspan, +svg .vue-flow__edge.cancel-edge .vue-flow__edgeLabel text, +svg .vue-flow__edge.cancel-edge .vue-flow__edgeLabel tspan, +svg .vue-flow__edge.cancel-edge .vue-flow__edge-textwrapper text, +svg .vue-flow__edge.cancel-edge .vue-flow__edge-textwrapper tspan { + fill: #000000 !important; + color: #000000 !important; + stroke: none !important; +} + +/* Hide label backgrounds */ +.vue-flow__edgeLabelbg, +.vue-flow__edge-textbg, +svg .vue-flow__edgeLabelbg, +svg .vue-flow__edge-textbg { + display: none !important; + opacity: 0 !important; +} + +/* Additional high-specificity selectors */ +html body .vue-flow__edgeLabel text, +html body .vue-flow__edgeLabel tspan, +html body .vue-flow__edge-textwrapper text, +html body .vue-flow__edge-textwrapper tspan, +html body svg .vue-flow__edgeLabel text, +html body svg .vue-flow__edgeLabel tspan, +html body svg .vue-flow__edge-textwrapper text, +html body svg .vue-flow__edge-textwrapper tspan { + fill: #000000 !important; + color: #000000 !important; + stroke: none !important; +} + +/* Target all text elements in Vue Flow */ +div[id*="vue-flow"] text, +div[id*="vue-flow"] tspan, +svg[id*="vue-flow"] text, +svg[id*="vue-flow"] tspan { + fill: #000000 !important; + color: #000000 !important; + stroke: none !important; +} + diff --git a/tools/axon_panel/src/main.js b/tools/axon_panel/src/main.js new file mode 100644 index 0000000..1bce120 --- /dev/null +++ b/tools/axon_panel/src/main.js @@ -0,0 +1,5 @@ +import { createApp } from 'vue' +import App from './App.vue' +import './edge-label-fix.css' + +createApp(App).mount('#app') diff --git a/tools/axon_panel/vite.config.js b/tools/axon_panel/vite.config.js new file mode 100644 index 0000000..5dc9fbd --- /dev/null +++ b/tools/axon_panel/vite.config.js @@ -0,0 +1,20 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + plugins: [vue()], + server: { + port: 3000, + host: '0.0.0.0', + proxy: { + '/rpc': { + target: 'http://localhost:8080', + changeOrigin: true + }, + '/health': { + target: 'http://localhost:8080', + changeOrigin: true + } + } + } +}) From a97b67570fa626d124d39698f59b5fbe27959a71 Mon Sep 17 00:00:00 2001 From: XU Liangwei Date: Fri, 6 Feb 2026 15:37:58 +0800 Subject: [PATCH 03/10] refactor: remove plugin example tests --- apps/plugin_example/CMakeLists.txt | 54 --- apps/plugin_example/plugin_loader_test.cpp | 312 ------------------ .../run_test_with_ros2_tools.sh | 104 ------ apps/plugin_example/simple_load_test.cpp | 95 ------ apps/plugin_example/test_with_ros2_tools.cpp | 183 ---------- 5 files changed, 748 deletions(-) delete mode 100644 apps/plugin_example/CMakeLists.txt delete mode 100644 apps/plugin_example/plugin_loader_test.cpp delete mode 100644 apps/plugin_example/run_test_with_ros2_tools.sh delete mode 100644 apps/plugin_example/simple_load_test.cpp delete mode 100644 apps/plugin_example/test_with_ros2_tools.cpp diff --git a/apps/plugin_example/CMakeLists.txt b/apps/plugin_example/CMakeLists.txt deleted file mode 100644 index a74c601..0000000 --- a/apps/plugin_example/CMakeLists.txt +++ /dev/null @@ -1,54 +0,0 @@ -cmake_minimum_required(VERSION 3.12) -project(axon_plugin_example) - -# Default to C++17 -if(NOT CMAKE_CXX_STANDARD) - set(CMAKE_CXX_STANDARD 17) -endif() - -if(NOT CMAKE_CXX_STANDARD_REQUIRED) - set(CMAKE_CXX_STANDARD_REQUIRED ON) -endif() - -# Compiler warnings -if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") - add_compile_options(-Wall -Wextra -Wpedantic) -endif() - -# Build the simple load test executable -add_executable(simple_load_test - simple_load_test.cpp - ../axon_recorder/plugin_loader.cpp -) - -target_include_directories(simple_load_test PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR} - ${CMAKE_CURRENT_SOURCE_DIR}/../axon_recorder -) - -target_link_libraries(simple_load_test - ${CMAKE_DL_LIBS} -) - -# Build the full plugin loader test executable -add_executable(plugin_loader_test - plugin_loader_test.cpp - ../axon_recorder/plugin_loader.cpp -) - -# Include directories -target_include_directories(plugin_loader_test PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR} - ${CMAKE_CURRENT_SOURCE_DIR}/../axon_recorder -) - -# Link libraries -target_link_libraries(plugin_loader_test - ${CMAKE_DL_LIBS} - pthread -) - -# Install -install(TARGETS simple_load_test plugin_loader_test - RUNTIME DESTINATION bin -) diff --git a/apps/plugin_example/plugin_loader_test.cpp b/apps/plugin_example/plugin_loader_test.cpp deleted file mode 100644 index 7d3a306..0000000 --- a/apps/plugin_example/plugin_loader_test.cpp +++ /dev/null @@ -1,312 +0,0 @@ -// SPDX-FileCopyrightText: 2026 ArcheBase -// -// SPDX-License-Identifier: MulanPSL-2.0 - -/** - * @file plugin_loader_test.cpp - * @brief Plugin loader test example for Axon middleware plugins - * - * This example demonstrates how to: - * 1. Load a ROS2 plugin dynamically - * 2. Initialize the plugin with configuration - * 3. Set message callback - * 4. Subscribe to topics - * 5. Spin the executor - * 6. Cleanup and unload - */ - -#include "../axon_recorder/plugin_loader.hpp" - -#include -#include -#include -#include - -using namespace axon; - -// Global flag for graceful shutdown -static std::sig_atomic_t g_running = 1; - -// Signal handler for Ctrl+C -void signal_handler(int signal) { - (void)signal; - std::cout << "\n[INFO] Shutdown signal received..." << std::endl; - g_running = 0; -} - -// ============================================================================= -// Message callback implementation -// ============================================================================= -void message_callback( - const char* topic_name, const uint8_t* message_data, size_t message_size, - const char* message_type, uint64_t timestamp, void* user_data -) { - (void)user_data; - - // Print message received info - std::cout << "[MSG] Topic: " << topic_name << " | Type: " << message_type - << " | Size: " << message_size << " bytes" - << " | Timestamp: " << timestamp << std::endl; - - // Print first 32 bytes of message data (hexdump style) - constexpr size_t MAX_DUMP = 32; - size_t dump_size = std::min(message_size, MAX_DUMP); - - std::cout << " Data: "; - for (size_t i = 0; i < dump_size; ++i) { - printf("%02x ", message_data[i]); - } - if (message_size > MAX_DUMP) { - std::cout << "..."; - } - std::cout << std::endl; -} - -// ============================================================================= -// Test scenarios -// ============================================================================= - -/** - * Test 1: Basic plugin loading and initialization - */ -bool test_load_and_init(PluginLoader& loader, const std::string& plugin_path) { - std::cout << "\n=== Test 1: Load and Initialize Plugin ===" << std::endl; - - // Load the plugin - auto plugin_name_opt = loader.load(plugin_path); - if (!plugin_name_opt) { - std::cerr << "[ERROR] Failed to load plugin: " << loader.get_last_error() << std::endl; - return false; - } - - std::string plugin_name = *plugin_name_opt; - std::cout << "[OK] Loaded plugin: " << plugin_name << std::endl; - - // Get descriptor - const auto* descriptor = loader.get_descriptor(plugin_name); - if (!descriptor) { - std::cerr << "[ERROR] Failed to get plugin descriptor" << std::endl; - return false; - } - - // Print plugin info - std::cout << "[INFO] Plugin Details:" << std::endl; - std::cout << " - ABI Version: " << descriptor->abi_version_major << "." - << descriptor->abi_version_minor << std::endl; - std::cout << " - Middleware: " << descriptor->middleware_name << std::endl; - std::cout << " - Version: " << descriptor->middleware_version << std::endl; - std::cout << " - Plugin: " << descriptor->plugin_version << std::endl; - - // Initialize plugin with JSON config - const char* config_json = R"({ - "node_name": "axon_plugin_test", - "use_sim_time": false - })"; - - auto* plugin = loader.get_plugin(plugin_name); - AxonStatus status = descriptor->vtable->init(config_json); - - if (status != AXON_SUCCESS) { - std::cerr << "[ERROR] Failed to initialize plugin, status: " << status << std::endl; - return false; - } - - plugin->initialized = true; - std::cout << "[OK] Plugin initialized" << std::endl; - - return true; -} - -/** - * Test 2: Subscribe to topics - */ -bool test_subscribe(PluginLoader& loader, const std::string& plugin_name) { - std::cout << "\n=== Test 2: Subscribe to Topics ===" << std::endl; - - const auto* descriptor = loader.get_descriptor(plugin_name); - if (!descriptor || !descriptor->vtable->subscribe) { - std::cerr << "[ERROR] Plugin does not support subscribe" << std::endl; - return false; - } - - // List of topics to subscribe - std::vector> topics = { - {"/imu/data", "sensor_msgs/msg/Imu"}, - {"/camera0/rgb", "sensor_msgs/msg/Image"}, - }; - - for (const auto& [topic, type] : topics) { - std::cout << "[INFO] Subscribing to: " << topic << " (" << type << ")" << std::endl; - - AxonStatus status = descriptor->vtable->subscribe( - topic.c_str(), - type.c_str(), - nullptr, // options_json (none for this test) - message_callback, - nullptr // user_data - ); - - if (status != AXON_SUCCESS) { - std::cout << "[WARN] Failed to subscribe to " << topic << ", status: " << status << std::endl; - // Continue with other topics - } else { - std::cout << "[OK] Subscribed to: " << topic << std::endl; - } - } - - return true; -} - -/** - * Test 3: Spin the plugin - */ -bool test_spin(PluginLoader& loader, const std::string& plugin_name, int seconds) { - std::cout << "\n=== Test 3: Spin Plugin (" << seconds << " seconds) ===" << std::endl; - - const auto* descriptor = loader.get_descriptor(plugin_name); - if (!descriptor || !descriptor->vtable->start) { - std::cerr << "[ERROR] Plugin does not support start" << std::endl; - return false; - } - - auto* plugin = loader.get_plugin(plugin_name); - - // Start spinning (this creates the executor thread) - std::cout << "[INFO] Starting plugin executor... (Press Ctrl+C to stop early)" << std::endl; - AxonStatus status = descriptor->vtable->start(); - - if (status != AXON_SUCCESS) { - std::cerr << "[ERROR] Failed to start spinning, status: " << status << std::endl; - return false; - } - - plugin->running = true; - - // Wait for the specified duration while the executor runs in background - auto start_time = std::chrono::steady_clock::now(); - - while (g_running) { - // Check timeout - auto elapsed = std::chrono::duration_cast( - std::chrono::steady_clock::now() - start_time - ) - .count(); - - if (elapsed >= seconds) { - break; - } - - // Sleep for a bit - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - } - - std::cout << "[INFO] Spin test completed" << std::endl; - - return true; -} - -/** - * Test 4: Cleanup and unload - */ -bool test_cleanup(PluginLoader& loader, const std::string& plugin_name) { - std::cout << "\n=== Test 4: Cleanup and Unload ===" << std::endl; - - const auto* descriptor = loader.get_descriptor(plugin_name); - auto* plugin = loader.get_plugin(plugin_name); - - // Shutdown plugin - if (plugin && plugin->running && descriptor->vtable->stop) { - std::cout << "[INFO] Shutting down plugin..." << std::endl; - AxonStatus status = descriptor->vtable->stop(); - - if (status != AXON_SUCCESS) { - std::cerr << "[WARN] Shutdown returned error: " << status << std::endl; - } - plugin->running = false; - } - - // Unload plugin - if (loader.unload(plugin_name)) { - std::cout << "[OK] Plugin unloaded" << std::endl; - return true; - } else { - std::cerr << "[ERROR] Failed to unload: " << loader.get_last_error() << std::endl; - return false; - } -} - -// ============================================================================= -// Main entry point -// ============================================================================= -int main(int argc, char* argv[]) { - std::cout << "========================================" << std::endl; - std::cout << " Axon Plugin Loader Test" << std::endl; - std::cout << "========================================" << std::endl; - - // Setup signal handler - std::signal(SIGINT, signal_handler); - std::signal(SIGTERM, signal_handler); - - // Parse command line arguments - // Plugin path must be provided via command line argument or environment variable - std::string plugin_path; - const char* env_path = std::getenv("AXON_ROS2_PLUGIN_PATH"); - if (env_path) { - plugin_path = env_path; - } else if (argc > 1) { - plugin_path = argv[1]; - } else { - std::cerr << "[ERROR] Plugin path must be provided via:" << std::endl; - std::cerr << " 1. Command line argument: ./plugin_loader_test " << std::endl; - std::cerr << " 2. Environment variable: export AXON_ROS2_PLUGIN_PATH=" << std::endl; - return 1; - } - int test_duration_seconds = 10; - - if (argc > 2) { - test_duration_seconds = std::stoi(argv[2]); - } - - std::cout << "[INFO] Plugin path: " << plugin_path << std::endl; - std::cout << "[INFO] Test duration: " << test_duration_seconds << " seconds" << std::endl; - - // Create plugin loader - PluginLoader loader; - - // Run tests - stop on first failure - // Test 1: Load and initialize (critical - must pass to continue) - if (!test_load_and_init(loader, plugin_path)) { - std::cerr << "\n[FAIL] Test 1 failed" << std::endl; - loader.unload_all(); - return 1; - } - - // Test 2: Subscribe - if (!test_subscribe(loader, "ROS2")) { - std::cerr << "\n[FAIL] Test 2 failed" << std::endl; - loader.unload_all(); - return 1; - } - - // Test 3: Spin - if (!test_spin(loader, "ROS2", test_duration_seconds)) { - std::cerr << "\n[FAIL] Test 3 failed" << std::endl; - loader.unload_all(); - return 1; - } - - // Test 4: Cleanup - if (!test_cleanup(loader, "ROS2")) { - std::cerr << "\n[FAIL] Test 4 failed" << std::endl; - loader.unload_all(); - return 1; - } - // Final cleanup - loader.unload_all(); - - std::cout << "\n========================================" << std::endl; - std::cout << " All tests PASSED" << std::endl; - std::cout << "========================================" << std::endl; - - return 0; -} diff --git a/apps/plugin_example/run_test_with_ros2_tools.sh b/apps/plugin_example/run_test_with_ros2_tools.sh deleted file mode 100644 index a3ac02d..0000000 --- a/apps/plugin_example/run_test_with_ros2_tools.sh +++ /dev/null @@ -1,104 +0,0 @@ -#!/bin/bash -# Demo script showing how to test the ROS2 plugin with ros2 topic pub CLI tool - -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" - -# Plugin path - use environment variable, command line argument, or default relative path -if [ -n "$AXON_ROS2_PLUGIN_PATH" ]; then - PLUGIN_PATH="$AXON_ROS2_PLUGIN_PATH" -elif [ -n "$1" ]; then - PLUGIN_PATH="$1" -else - # Default relative path from project root - PLUGIN_PATH="${PROJECT_ROOT}/middlewares/ros2/install/axon_ros2_plugin/lib/axon/plugins/libaxon_ros2_plugin.so" -fi - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -echo -e "${GREEN}========================================" -echo " ROS2 Plugin CLI Tools Test" -echo "========================================${NC}" -echo "" -echo "This test will:" -echo " 1. Start the plugin subscriber in background" -echo " 2. Publish messages using 'ros2 topic pub'" -echo " 3. Verify the plugin receives the messages" -echo "" -echo "Plugin path: $PLUGIN_PATH" -echo "" - -# Source ROS2 -source /opt/ros/humble/setup.bash - -cd "$SCRIPT_DIR" - -# Check if plugin exists -if [ ! -f "$PLUGIN_PATH" ]; then - echo -e "${RED}[ERROR] Plugin not found: $PLUGIN_PATH${NC}" - exit 1 -fi - -# Start the subscriber in background -echo -e "${YELLOW}[INFO] Starting plugin subscriber...${NC}" -./build/test_with_ros2_tools "$PLUGIN_PATH" 20 > /tmp/plugin_test_output.txt 2>&1 & -SUBSCRIBER_PID=$! - -# Wait for plugin to initialize -sleep 2 - -# Check if subscriber is still running -if ! kill -0 $SUBSCRIBER_PID 2>/dev/null; then - echo -e "${RED}[ERROR] Subscriber failed to start${NC}" - cat /tmp/plugin_test_output.txt - exit 1 -fi - -echo -e "${GREEN}[OK] Subscriber started (PID: $SUBSCRIBER_PID)${NC}" -echo "" - -# Publish some messages -echo -e "${YELLOW}========================================" -echo " Publishing Messages" -echo "========================================${NC}" -echo "" - -for i in {1..5}; do - echo -e "${YELLOW}[PUBLISH] Sending message $i...${NC}" - ros2 topic pub --once /chatter std_msgs/String "data: 'Hello from CLI $i'" >/dev/null 2>&1 & - sleep 0.5 -done - -echo "" -echo -e "${GREEN}[INFO] Published 5 messages${NC}" -echo "" - -# Wait for subscriber to process all messages -echo -e "${YELLOW}[INFO] Waiting for subscriber to finish (20 seconds)...${NC}" -wait $SUBSCRIBER_PID 2>/dev/null || true -EXIT_CODE=$? - -# Display results -cat /tmp/plugin_test_output.txt - -echo "" -echo -e "${GREEN}========================================" -echo " Test Complete" -echo "========================================${NC}" -echo "" - -# Check if any messages were received -if grep -q "MSG 1" /tmp/plugin_test_output.txt; then - MESSAGE_COUNT=$(grep -c "^\\[MSG" /tmp/plugin_test_output.txt || echo "0") - echo -e "${GREEN}[SUCCESS] Plugin received $MESSAGE_COUNT messages!${NC}" - exit 0 -else - echo -e "${RED}[FAIL] No messages received${NC}" - exit 1 -fi diff --git a/apps/plugin_example/simple_load_test.cpp b/apps/plugin_example/simple_load_test.cpp deleted file mode 100644 index 1483c57..0000000 --- a/apps/plugin_example/simple_load_test.cpp +++ /dev/null @@ -1,95 +0,0 @@ -// SPDX-FileCopyrightText: 2026 ArcheBase -// -// SPDX-License-Identifier: MulanPSL-2.0 - -/** - * @file simple_load_test.cpp - * @brief Minimal test to verify plugin can be loaded and descriptor accessed - * - * This is a simplified test that only checks: - * 1. The plugin library can be opened with dlopen - * 2. The axon_get_plugin_descriptor symbol exists - * 3. The descriptor is valid - * - * This does NOT require ROS2 to be running. - */ - -#include - -#include "../axon_recorder/plugin_loader.hpp" - -using namespace axon; - -int main(int argc, char* argv[]) { - std::cout << "=== Simple Plugin Load Test ===" << std::endl; - - // Parse arguments - if (argc < 2) { - std::cerr << "Usage: " << argv[0] << " " << std::endl; - std::cerr << "Example: " << argv[0] << " /path/to/libaxon_ros2_plugin.so" << std::endl; - return 1; - } - - std::string plugin_path = argv[1]; - std::cout << "Plugin path: " << plugin_path << std::endl << std::endl; - - // Create loader - PluginLoader loader; - - // Test 1: Load plugin - std::cout << "[TEST 1] Loading plugin..." << std::endl; - auto plugin_name_opt = loader.load(plugin_path); - if (!plugin_name_opt) { - std::cerr << "[FAIL] Could not load plugin" << std::endl; - std::cerr << "Error: " << loader.get_last_error() << std::endl; - return 1; - } - std::cout << "[PASS] Plugin loaded: " << *plugin_name_opt << std::endl << std::endl; - - // Test 2: Get descriptor - std::cout << "[TEST 2] Getting descriptor..." << std::endl; - const auto* descriptor = loader.get_descriptor(*plugin_name_opt); - if (!descriptor) { - std::cerr << "[FAIL] Could not get descriptor" << std::endl; - return 1; - } - std::cout << "[PASS] Descriptor found" << std::endl; - - // Print plugin info - std::cout << "\nPlugin Information:" << std::endl; - std::cout << " ABI Version: " << descriptor->abi_version_major << "." - << descriptor->abi_version_minor << std::endl; - std::cout << " Middleware: " - << (descriptor->middleware_name ? descriptor->middleware_name : "N/A") << std::endl; - std::cout << " Middleware Version: " - << (descriptor->middleware_version ? descriptor->middleware_version : "N/A") - << std::endl; - std::cout << " Plugin Version: " - << (descriptor->plugin_version ? descriptor->plugin_version : "N/A") << std::endl; - - // Test 3: Check vtable - std::cout << "\n[TEST 3] Checking vtable..." << std::endl; - if (!descriptor->vtable) { - std::cerr << "[FAIL] Vtable is null" << std::endl; - return 1; - } - std::cout << "[PASS] Vtable exists" << std::endl; - - std::cout << "\nVtable Functions:" << std::endl; - std::cout << " init: " << (descriptor->vtable->init ? "✓" : "✗") << std::endl; - std::cout << " start: " << (descriptor->vtable->start ? "✓" : "✗") << std::endl; - std::cout << " stop: " << (descriptor->vtable->stop ? "✓" : "✗") << std::endl; - std::cout << " subscribe: " << (descriptor->vtable->subscribe ? "✓" : "✗") << std::endl; - std::cout << " publish: " << (descriptor->vtable->publish ? "✓" : "✗") << std::endl; - - // Test 4: Unload - std::cout << "\n[TEST 4] Unloading plugin..." << std::endl; - if (!loader.unload(*plugin_name_opt)) { - std::cerr << "[FAIL] Could not unload plugin" << std::endl; - return 1; - } - std::cout << "[PASS] Plugin unloaded" << std::endl; - - std::cout << "\n=== All Tests PASSED ===" << std::endl; - return 0; -} diff --git a/apps/plugin_example/test_with_ros2_tools.cpp b/apps/plugin_example/test_with_ros2_tools.cpp deleted file mode 100644 index 3629be2..0000000 --- a/apps/plugin_example/test_with_ros2_tools.cpp +++ /dev/null @@ -1,183 +0,0 @@ -/** - * @file test_with_ros2_tools.cpp - * @brief Test plugin by subscribing to messages published via ros2 topic pub CLI tool - * - * Usage: - * 1. Run this program: ./test_with_ros2_tools - * 2. In another terminal, publish messages: - * ros2 topic pub /chatter std_msgs/String "data: 'Hello World'" - */ - -#include -#include -#include -#include -#include - -#include "../axon_recorder/plugin_loader.hpp" - -using namespace axon; - -static std::sig_atomic_t g_running = 1; -static std::atomic g_message_count{0}; - -void signal_handler(int signal) { - (void)signal; - std::cout << "\n[INFO] Shutdown signal received..." << std::endl; - g_running = 0; -} - -void message_callback( - const char* topic_name, const uint8_t* message_data, size_t message_size, - const char* message_type, uint64_t timestamp, void* user_data -) { - (void)user_data; - g_message_count++; - - std::cout << "[MSG " << g_message_count << "] Topic: " << topic_name - << " | Type: " << message_type << " | Size: " << message_size << " bytes" - << " | Timestamp: " << timestamp << std::endl; - - // Print first 16 bytes - constexpr size_t MAX_DUMP = 16; - size_t dump_size = std::min(message_size, MAX_DUMP); - std::cout << " Data: "; - for (size_t i = 0; i < dump_size; ++i) { - printf("%02x ", message_data[i]); - } - if (message_size > MAX_DUMP) { - std::cout << "..."; - } - std::cout << std::endl; -} - -int main(int argc, char* argv[]) { - std::cout << "========================================" << std::endl; - std::cout << " ROS2 Plugin Test with CLI Tools" << std::endl; - std::cout << "========================================" << std::endl; - - // Parse arguments - // Default plugin path - can be overridden via command line argument or environment variable - std::string plugin_path; - const char* env_path = std::getenv("AXON_ROS2_PLUGIN_PATH"); - if (env_path) { - plugin_path = env_path; - } else if (argc > 1) { - plugin_path = argv[1]; - } else { - std::cerr << "[ERROR] Plugin path must be provided via:" << std::endl; - std::cerr << " 1. Command line argument: ./test_with_ros2_tools " << std::endl; - std::cerr << " 2. Environment variable: export AXON_ROS2_PLUGIN_PATH=" << std::endl; - return 1; - } - int wait_seconds = 30; - - if (argc > 2) { - wait_seconds = std::atoi(argv[2]); - } - - std::cout << "[INFO] Plugin path: " << plugin_path << std::endl; - std::cout << "[INFO] Wait time: " << wait_seconds << " seconds" << std::endl; - - // Setup signal handler - std::signal(SIGINT, signal_handler); - std::signal(SIGTERM, signal_handler); - - // Create loader - PluginLoader loader; - - // Load plugin - std::cout << "\n[INFO] Loading plugin..." << std::endl; - auto plugin_name_opt = loader.load(plugin_path); - - if (!plugin_name_opt) { - std::cerr << "[ERROR] Failed to load plugin: " << loader.get_last_error() << std::endl; - return 1; - } - - std::string plugin_name = *plugin_name_opt; - std::cout << "[OK] Loaded plugin: " << plugin_name << std::endl; - - // Initialize plugin - const auto* descriptor = loader.get_descriptor(plugin_name); - auto* plugin = loader.get_plugin(plugin_name); - - const char* config_json = "{}"; - AxonStatus status = descriptor->vtable->init(config_json); - - if (status != AXON_SUCCESS) { - std::cerr << "[ERROR] Failed to initialize plugin, status: " << status << std::endl; - loader.unload_all(); - return 1; - } - - plugin->initialized = true; - std::cout << "[OK] Plugin initialized" << std::endl; - - // Subscribe to topic - std::cout << "[INFO] Subscribing to /chatter..." << std::endl; - status = - descriptor->vtable->subscribe("/chatter", "std_msgs/msg/String", message_callback, nullptr); - - if (status != AXON_SUCCESS) { - std::cerr << "[ERROR] Failed to subscribe, status: " << status << std::endl; - loader.unload_all(); - return 1; - } - - std::cout << "[OK] Subscribed to /chatter" << std::endl; - - // Start spinning - std::cout << "[INFO] Starting plugin..." << std::endl; - status = descriptor->vtable->start(); - - if (status != AXON_SUCCESS) { - std::cerr << "[ERROR] Failed to start plugin, status: " << status << std::endl; - loader.unload_all(); - return 1; - } - - plugin->running = true; - std::cout << "[OK] Plugin started" << std::endl; - - // Wait for messages - std::cout << "\n========================================" << std::endl; - std::cout << " Ready to receive messages" << std::endl; - std::cout << "========================================" << std::endl; - std::cout << "\nIn another terminal, run:" << std::endl; - std::cout << " ros2 topic pub /chatter std_msgs/String \"data: 'Hello World'\"" << std::endl; - std::cout << "\nWaiting " << wait_seconds << " seconds (or press Ctrl+C to stop early)..." - << std::endl; - std::cout << std::endl; - - auto start_time = std::chrono::steady_clock::now(); - while (g_running) { - auto elapsed = std::chrono::duration_cast( - std::chrono::steady_clock::now() - start_time - ) - .count(); - - if (elapsed >= wait_seconds) { - break; - } - - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - } - - // Shutdown - std::cout << "\n[INFO] Shutting down..." << std::endl; - if (descriptor->vtable->stop) { - descriptor->vtable->stop(); - } - plugin->running = false; - - loader.unload_all(); - std::cout << "[OK] Plugin unloaded" << std::endl; - - std::cout << "\n========================================" << std::endl; - std::cout << " Test Complete" << std::endl; - std::cout << " Messages received: " << g_message_count << std::endl; - std::cout << "========================================" << std::endl; - - return (g_message_count > 0) ? 0 : 1; -} From 6bf825abe60749ac21e96088874392ea84a8da70 Mon Sep 17 00:00:00 2001 From: XU Liangwei Date: Fri, 6 Feb 2026 15:39:23 +0800 Subject: [PATCH 04/10] refactor: move axon_panel from tools to apps --- {tools => apps}/axon_panel/.gitignore | 0 {tools => apps}/axon_panel/README.md | 0 {tools => apps}/axon_panel/index.html | 0 {tools => apps}/axon_panel/package-lock.json | 0 {tools => apps}/axon_panel/package.json | 0 {tools => apps}/axon_panel/run.sh | 0 {tools => apps}/axon_panel/src/App.vue | 0 {tools => apps}/axon_panel/src/api/rpc.js | 0 {tools => apps}/axon_panel/src/components/ConfigPanel.vue | 0 {tools => apps}/axon_panel/src/components/ConnectionStatus.vue | 0 {tools => apps}/axon_panel/src/components/ControlPanel.vue | 0 {tools => apps}/axon_panel/src/components/LogPanel.vue | 0 {tools => apps}/axon_panel/src/components/StateMachineDiagram.vue | 0 {tools => apps}/axon_panel/src/components/StatePanel.vue | 0 {tools => apps}/axon_panel/src/edge-label-fix.css | 0 {tools => apps}/axon_panel/src/main.js | 0 {tools => apps}/axon_panel/vite.config.js | 0 17 files changed, 0 insertions(+), 0 deletions(-) rename {tools => apps}/axon_panel/.gitignore (100%) rename {tools => apps}/axon_panel/README.md (100%) rename {tools => apps}/axon_panel/index.html (100%) rename {tools => apps}/axon_panel/package-lock.json (100%) rename {tools => apps}/axon_panel/package.json (100%) rename {tools => apps}/axon_panel/run.sh (100%) rename {tools => apps}/axon_panel/src/App.vue (100%) rename {tools => apps}/axon_panel/src/api/rpc.js (100%) rename {tools => apps}/axon_panel/src/components/ConfigPanel.vue (100%) rename {tools => apps}/axon_panel/src/components/ConnectionStatus.vue (100%) rename {tools => apps}/axon_panel/src/components/ControlPanel.vue (100%) rename {tools => apps}/axon_panel/src/components/LogPanel.vue (100%) rename {tools => apps}/axon_panel/src/components/StateMachineDiagram.vue (100%) rename {tools => apps}/axon_panel/src/components/StatePanel.vue (100%) rename {tools => apps}/axon_panel/src/edge-label-fix.css (100%) rename {tools => apps}/axon_panel/src/main.js (100%) rename {tools => apps}/axon_panel/vite.config.js (100%) diff --git a/tools/axon_panel/.gitignore b/apps/axon_panel/.gitignore similarity index 100% rename from tools/axon_panel/.gitignore rename to apps/axon_panel/.gitignore diff --git a/tools/axon_panel/README.md b/apps/axon_panel/README.md similarity index 100% rename from tools/axon_panel/README.md rename to apps/axon_panel/README.md diff --git a/tools/axon_panel/index.html b/apps/axon_panel/index.html similarity index 100% rename from tools/axon_panel/index.html rename to apps/axon_panel/index.html diff --git a/tools/axon_panel/package-lock.json b/apps/axon_panel/package-lock.json similarity index 100% rename from tools/axon_panel/package-lock.json rename to apps/axon_panel/package-lock.json diff --git a/tools/axon_panel/package.json b/apps/axon_panel/package.json similarity index 100% rename from tools/axon_panel/package.json rename to apps/axon_panel/package.json diff --git a/tools/axon_panel/run.sh b/apps/axon_panel/run.sh similarity index 100% rename from tools/axon_panel/run.sh rename to apps/axon_panel/run.sh diff --git a/tools/axon_panel/src/App.vue b/apps/axon_panel/src/App.vue similarity index 100% rename from tools/axon_panel/src/App.vue rename to apps/axon_panel/src/App.vue diff --git a/tools/axon_panel/src/api/rpc.js b/apps/axon_panel/src/api/rpc.js similarity index 100% rename from tools/axon_panel/src/api/rpc.js rename to apps/axon_panel/src/api/rpc.js diff --git a/tools/axon_panel/src/components/ConfigPanel.vue b/apps/axon_panel/src/components/ConfigPanel.vue similarity index 100% rename from tools/axon_panel/src/components/ConfigPanel.vue rename to apps/axon_panel/src/components/ConfigPanel.vue diff --git a/tools/axon_panel/src/components/ConnectionStatus.vue b/apps/axon_panel/src/components/ConnectionStatus.vue similarity index 100% rename from tools/axon_panel/src/components/ConnectionStatus.vue rename to apps/axon_panel/src/components/ConnectionStatus.vue diff --git a/tools/axon_panel/src/components/ControlPanel.vue b/apps/axon_panel/src/components/ControlPanel.vue similarity index 100% rename from tools/axon_panel/src/components/ControlPanel.vue rename to apps/axon_panel/src/components/ControlPanel.vue diff --git a/tools/axon_panel/src/components/LogPanel.vue b/apps/axon_panel/src/components/LogPanel.vue similarity index 100% rename from tools/axon_panel/src/components/LogPanel.vue rename to apps/axon_panel/src/components/LogPanel.vue diff --git a/tools/axon_panel/src/components/StateMachineDiagram.vue b/apps/axon_panel/src/components/StateMachineDiagram.vue similarity index 100% rename from tools/axon_panel/src/components/StateMachineDiagram.vue rename to apps/axon_panel/src/components/StateMachineDiagram.vue diff --git a/tools/axon_panel/src/components/StatePanel.vue b/apps/axon_panel/src/components/StatePanel.vue similarity index 100% rename from tools/axon_panel/src/components/StatePanel.vue rename to apps/axon_panel/src/components/StatePanel.vue diff --git a/tools/axon_panel/src/edge-label-fix.css b/apps/axon_panel/src/edge-label-fix.css similarity index 100% rename from tools/axon_panel/src/edge-label-fix.css rename to apps/axon_panel/src/edge-label-fix.css diff --git a/tools/axon_panel/src/main.js b/apps/axon_panel/src/main.js similarity index 100% rename from tools/axon_panel/src/main.js rename to apps/axon_panel/src/main.js diff --git a/tools/axon_panel/vite.config.js b/apps/axon_panel/vite.config.js similarity index 100% rename from tools/axon_panel/vite.config.js rename to apps/axon_panel/vite.config.js From 4fd36dd6e1019fba531278aa5599cdd4e8fe4c37 Mon Sep 17 00:00:00 2001 From: XU Liangwei Date: Fri, 6 Feb 2026 15:40:05 +0800 Subject: [PATCH 05/10] feat: add placeholder apps for axon_config and axon_transfer --- CLAUDE.md | 141 ++++++++++++++++++++++++++++-- apps/axon_config/CMakeLists.txt | 8 ++ apps/axon_config/README.md | 17 ++++ apps/axon_config/src/main.cpp | 9 ++ apps/axon_transfer/CMakeLists.txt | 8 ++ apps/axon_transfer/README.md | 16 ++++ apps/axon_transfer/src/main.cpp | 9 ++ 7 files changed, 199 insertions(+), 9 deletions(-) create mode 100644 apps/axon_config/CMakeLists.txt create mode 100644 apps/axon_config/README.md create mode 100644 apps/axon_config/src/main.cpp create mode 100644 apps/axon_transfer/CMakeLists.txt create mode 100644 apps/axon_transfer/README.md create mode 100644 apps/axon_transfer/src/main.cpp diff --git a/CLAUDE.md b/CLAUDE.md index 0a5d7f2..0d7748c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -103,17 +103,115 @@ make coverage-html make clean-coverage ``` +### Web Control Panel Development + +```bash +# Build and run axon_panel web interface +cd apps/axon_panel +npm install # Install dependencies (first time only) +npm run dev # Start development server (http://localhost:5173) +npm run build # Build for production (outputs to dist/) +npm run preview # Preview production build +``` + +## Application Architecture + +Axon consists of four main components that work together: + +| Component | Purpose | Technology | Location | +|-----------|---------|------------|----------| +| **axon_recorder** | Core recording engine with HTTP RPC API | C++17 | [apps/axon_recorder/](apps/axon_recorder/) | +| **axon_transfer** | S3 transfer daemon (standalone) | C++17 | [apps/axon_transfer/](apps/axon_transfer/) | +| **axon_panel** | Web-based control UI (frontend only) | Vue 3 + Vite | [apps/axon_panel/](apps/axon_panel/) | +| **axon_config** | Robot initialization and config collection | C++17 | [apps/axon_config/](apps/axon_config/) | + +### Application Interaction + +``` +┌─────────────────────┐ +│ axon_config │ Robot initialization (one-time setup) +│ (CLI Tool) │ → Collects robot type, SN, sensor config, URDF +└─────────────────────┘ + │ + │ Config files + ▼ +┌─────────────────────┐ HTTP RPC ┌─────────────────────┐ +│ axon_panel │◄──────────────────►│ axon_recorder │ +│ (Vue 3 Web UI) │ State Control │ (C++ Backend) │ +│ - Monitor state │ - config │ - HTTP RPC Server │ +│ - Control buttons │ - begin │ - Plugin Loader │ +│ - View stats │ - pause/resume │ - MCAP Writer │ +│ - Activity log │ - finish/cancel │ - Worker Threads │ +└─────────────────────┘ └──────────┬──────────┘ + │ + │ Upload requests + ▼ + ┌─────────────────────┐ + │ axon_transfer │ + │ (Transfer Daemon) │ + │ - S3 multipart │ + │ - Retry logic │ + │ - State recovery │ + └──────────┬──────────┘ + │ + ▼ + ┌─────────────────────┐ + │ S3 Storage │ + └─────────────────────┘ +``` + +**Panel Control Flow:** `axon_panel` (Vue 3 frontend) sends HTTP RPC commands to `axon_recorder` to control state transitions: +- `POST /rpc/config` → IDLE → READY +- `POST /rpc/begin` → READY → RECORDING +- `POST /rpc/pause` → RECORDING → PAUSED +- `POST /rpc/resume` → PAUSED → RECORDING +- `POST /rpc/finish` → RECORDING/PAUSED → IDLE + +**Note on Current Status:** +- `axon_recorder`: Fully implemented C++ application +- `axon_transfer`: Standalone daemon pending design; currently uses [core/axon_uploader/](core/axon_uploader/) library integrated into recorder +- `axon_panel`: Fully implemented Vue 3 SPA at [apps/axon_panel/](apps/axon_panel/) +- `axon_config`: Placeholder only; CLI interface and functionality pending design + +### axon_panel - Web Control Panel + +**Location:** [apps/axon_panel/](apps/axon_panel/) + +**Purpose:** Browser-based interface for monitoring and controlling the recorder + +**Features:** +- Real-time state monitoring and statistics +- Visual state machine diagram with Vue Flow +- Recording control (config/begin/pause/resume/finish/cancel) +- Activity logging with color-coded messages +- Responsive design (desktop + mobile) + +**Development:** +```bash +cd apps/axon_panel +npm install +npm run dev # Development server (http://localhost:5173) +npm run build # Production build to dist/ +``` + +**Key Files:** +- `apps/axon_panel/src/App.vue` - Root component with state management +- `apps/axon_panel/src/api/rpc.js` - RPC API client +- `apps/axon_panel/src/components/` - Vue components (StatePanel, ControlPanel, etc.) + +**See:** [docs/designs/frontend-design.md](docs/designs/frontend-design.md) for complete architecture + ## High-Level Architecture The system follows a layered architecture with a 4-state task-centric FSM and a plugin-based middleware integration layer: ``` -Server/Fleet Manager (ros-bridge) → Recording Services → State Machine → MCAP Writer - ↓ ↓ ↓ - HTTP Callbacks Worker Threads SPSC Queues - ↓ ↓ ↓ - CachedRecordingConfig Start/Finish Lock-free - (YAML Configuration) Notify Message Transfer +Server/Fleet Manager → Recording Services → State Machine → MCAP Writer + ↓ ↓ ↓ ↓ + HTTP RPC API HTTP Callbacks Worker Threads SPSC Queues + ↓ ↓ ↓ ↓ + Task Config Start/Finish Lock-free Message Transfer + (YAML) Notify Transfer ``` ### Plugin-Based Middleware Architecture @@ -132,7 +230,7 @@ Axon/ ├── core/ # Middleware-agnostic core libraries │ ├── axon_mcap/ # MCAP writer (no ROS dependencies) │ ├── axon_logging/ # Logging infrastructure (no ROS dependencies) -│ └── axon_uploader/ # S3 uploader (no ROS dependencies) +│ └── axon_uploader/ # S3 uploader library (no ROS dependencies) │ ├── middlewares/ # Middleware-specific plugins and filters │ ├── ros1/ # ROS1 (Noetic) plugin → libaxon_ros1.so @@ -146,8 +244,12 @@ Axon/ │ ├── axon_recorder/ # Plugin loader and HTTP RPC server │ └── plugin_example/ # Example plugin implementation │ +├── tools/ # Utility tools and web applications +│ └── axon_panel/ # Vue 3 web control panel +│ └── docs/designs/ # Design documents ├── rpc-api-design.md # HTTP RPC API specification + ├── frontend-design.md # AxonPanel web UI architecture └── depth-compression-filter.md # Depth compression design ``` @@ -609,8 +711,10 @@ grep -r "AXON_ROS1\|AXON_ROS2" build/ | Zenoh plugin | `middlewares/zenoh/` (CMake) | | Filters | `middlewares/filters/` (shared data processing) | | Depth compression | `middlewares/filters/depthlitez/` | -| Main app (HTTP RPC) | `apps/axon_recorder/` | -| Plugin example | `apps/plugin_example/` | +| Recorder app (HTTP RPC) | `apps/axon_recorder/` | +| Transfer daemon | `apps/axon_transfer/` | +| Web control panel | `apps/axon_panel/` (Vue 3) | +| Config tool CLI | `apps/axon_config/` | | Plugin ABI interface | `apps/axon_recorder/plugin_loader.hpp` | | Tests | `*/test/` or `*/test_*.cpp` | | CMake modules | `cmake/` | @@ -629,3 +733,22 @@ To create a new middleware plugin: - `AxonPluginDescriptor` contains `abi_version_major` and `abi_version_minor` - Always verify compatibility before loading plugins - Reserve space in vtable for future extensions + +### Application Development Workflows + +**Current Status:** +- `axon_panel`: Fully implemented Vue 3 SPA at [apps/axon_panel/](apps/axon_panel/) +- `axon_transfer`: Standalone daemon pending design; currently uses [core/axon_uploader/](core/axon_uploader/) library integrated into recorder +- `axon_config`: Placeholder only; CLI interface and functionality pending design + +**axon_panel Development:** +```bash +# Terminal 1: Start recorder +./build/axon_recorder/axon_recorder --plugin ./build/middlewares/libaxon_ros2.so + +# Terminal 2: Start panel dev server +cd apps/axon_panel +npm run dev # Starts at http://localhost:5173 +``` + +**See:** [docs/designs/frontend-design.md](docs/designs/frontend-design.md) for complete panel architecture diff --git a/apps/axon_config/CMakeLists.txt b/apps/axon_config/CMakeLists.txt new file mode 100644 index 0000000..607f45b --- /dev/null +++ b/apps/axon_config/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.16) +project(axon_config LANGUAGES CXX) + +# C++17 standard +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# TODO: Add executable, CLI argument parsing, and configuration modules diff --git a/apps/axon_config/README.md b/apps/axon_config/README.md new file mode 100644 index 0000000..239c4ce --- /dev/null +++ b/apps/axon_config/README.md @@ -0,0 +1,17 @@ +# Axon Config Application + +**Status:** Placeholder - Design pending + +## Purpose + +Robot initialization and configuration collection tool. + +## Design Notes + +TODO: Define CLI interface for: +- Robot type discovery +- Serial number collection +- Sensor configuration +- URDF file collection + +This tool will implement configuration management functionality directly (no separate library). diff --git a/apps/axon_config/src/main.cpp b/apps/axon_config/src/main.cpp new file mode 100644 index 0000000..2215116 --- /dev/null +++ b/apps/axon_config/src/main.cpp @@ -0,0 +1,9 @@ +// Axon Config - Main Entry Point +// Placeholder implementation - Design pending + +#include + +int main(int argc, char** argv) { + std::cout << "Axon Config - Robot initialization tool - Design pending\n"; + return 0; +} diff --git a/apps/axon_transfer/CMakeLists.txt b/apps/axon_transfer/CMakeLists.txt new file mode 100644 index 0000000..f9b4d63 --- /dev/null +++ b/apps/axon_transfer/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.16) +project(axon_transfer LANGUAGES CXX) + +# C++17 standard +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# TODO: Add executable, dependencies, and installation rules diff --git a/apps/axon_transfer/README.md b/apps/axon_transfer/README.md new file mode 100644 index 0000000..ff95499 --- /dev/null +++ b/apps/axon_transfer/README.md @@ -0,0 +1,16 @@ +# Axon Transfer Application + +**Status:** Placeholder - Design pending + +## Purpose + +Standalone S3 transfer daemon application for uploading MCAP files to cloud storage. + +## Design Notes + +TODO: Define application architecture, CLI interface, and configuration format. + +## Related Components + +- Core library: [core/axon_uploader/](../../core/axon_uploader/) +- Design doc: [docs/designs/edge-uploader-design.md](../../docs/designs/edge-uploader-design.md) diff --git a/apps/axon_transfer/src/main.cpp b/apps/axon_transfer/src/main.cpp new file mode 100644 index 0000000..665b610 --- /dev/null +++ b/apps/axon_transfer/src/main.cpp @@ -0,0 +1,9 @@ +// Axon Transfer - Main Entry Point +// Placeholder implementation - Design pending + +#include + +int main(int argc, char** argv) { + std::cout << "Axon Transfer - S3 transfer daemon - Design pending\n"; + return 0; +} From 90c0f8399861e3f8896c642ba9f53ffb3ff2a92c Mon Sep 17 00:00:00 2001 From: Liangwei XU Date: Mon, 9 Feb 2026 12:28:41 +0800 Subject: [PATCH 06/10] fix: add SPDX license headers to mock plugin and placeholder apps --- Makefile | 10 ---------- REUSE.toml | 11 +++++++++-- apps/axon_config/src/main.cpp | 10 ++++++++-- apps/axon_transfer/src/main.cpp | 10 ++++++++-- .../mock/src/mock_plugin/include/mock_plugin.hpp | 6 ++++++ .../mock/src/mock_plugin/src/mock_plugin.cpp | 6 ++++++ .../src/mock_plugin/src/mock_plugin_export.cpp | 15 ++++++++++++--- .../src/mock_plugin/test/test_mock_plugin_e2e.cpp | 6 ++++++ .../mock_plugin/test/test_mock_plugin_load.cpp | 9 ++++++++- 9 files changed, 63 insertions(+), 20 deletions(-) diff --git a/Makefile b/Makefile index e675219..c5c00d5 100644 --- a/Makefile +++ b/Makefile @@ -227,17 +227,10 @@ test-mcap: build-mcap test-uploader: build-core @printf "%s\n" "$(YELLOW)Running axon_uploader tests...$(NC)" @if [ -d "$(BUILD_DIR)/axon_uploader" ]; then \ -<<<<<<< HEAD cd $(BUILD_DIR)/axon_uploader && ctest --output-on-failure && \ printf "%s\n" "$(GREEN)✓ axon_uploader tests passed$(NC)"; \ else \ printf "%s\n" "$(YELLOW)⚠ axon_uploader not built (requires AWS SDK, enable with -DAXON_BUILD_UPLOADER=ON)$(NC)"; \ -======= - cd $(BUILD_DIR)/axon_uploader && ctest --output-on-failure; \ - printf "%s\n" "$(GREEN)✓ axon_uploader tests passed$(NC)"; \ - else \ - printf "%s\n" "$(YELLOW)⚠ axon_uploader not built, skipping tests$(NC)"; \ ->>>>>>> 5bf3c1b (feat(mock_plugin): Implement mock plugin with C ABI interface and E2E tests) fi # Test axon_logging @@ -537,12 +530,9 @@ test: test-core clean: @printf "%s\n" "$(YELLOW)Cleaning build artifacts...$(NC)" @rm -rf $(BUILD_DIR) $(COVERAGE_DIR) -<<<<<<< HEAD -======= @cd middlewares/ros2 && rm -rf build install log 2>/dev/null || true @cd middlewares/ros1 && catkin clean --yes 2>/dev/null || true @rm -rf middlewares/mock/src/mock_plugin/build middlewares/mock/install 2>/dev/null || true ->>>>>>> 5bf3c1b (feat(mock_plugin): Implement mock plugin with C ABI interface and E2E tests) @printf "%s\n" "$(GREEN)✓ All build artifacts cleaned$(NC)" # Install target diff --git a/REUSE.toml b/REUSE.toml index 2b732ba..d915ad3 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -161,8 +161,8 @@ SPDX-License-Identifier = "NO-COPYRIGHT" [[annotations]] path = "**/*_mock.*" -SPDX-FileCopyrightText = "NO-COPYRIGHT" -SPDX-License-Identifier = "NO-COPYRIGHT" +SPDX-FileCopyrightText = "Copyright (c) 2026 ArcheBase" +SPDX-License-Identifier = "MulanPSL-2.0" # Ignore dependency directories [[annotations]] @@ -174,3 +174,10 @@ SPDX-License-Identifier = "NO-COPYRIGHT" path = "install/**" SPDX-FileCopyrightText = "NO-COPYRIGHT" SPDX-License-Identifier = "NO-COPYRIGHT" + +# Frontend assets (JavaScript, CSS, HTML, Vue components) +[[annotations]] +path = "apps/axon_panel/**" +precedence = "override" +SPDX-FileCopyrightText = "Copyright (c) 2026 ArcheBase" +SPDX-License-Identifier = "MulanPSL-2.0" diff --git a/apps/axon_config/src/main.cpp b/apps/axon_config/src/main.cpp index 2215116..bb59ddc 100644 --- a/apps/axon_config/src/main.cpp +++ b/apps/axon_config/src/main.cpp @@ -1,9 +1,15 @@ +/* + * SPDX-FileCopyrightText: 2026 ArcheBase + * + * SPDX-License-Identifier: MulanPSL-2.0 + */ + // Axon Config - Main Entry Point // Placeholder implementation - Design pending #include int main(int argc, char** argv) { - std::cout << "Axon Config - Robot initialization tool - Design pending\n"; - return 0; + std::cout << "Axon Config - Robot initialization tool - Design pending\n"; + return 0; } diff --git a/apps/axon_transfer/src/main.cpp b/apps/axon_transfer/src/main.cpp index 665b610..da4cca4 100644 --- a/apps/axon_transfer/src/main.cpp +++ b/apps/axon_transfer/src/main.cpp @@ -1,9 +1,15 @@ +/* + * SPDX-FileCopyrightText: 2026 ArcheBase + * + * SPDX-License-Identifier: MulanPSL-2.0 + */ + // Axon Transfer - Main Entry Point // Placeholder implementation - Design pending #include int main(int argc, char** argv) { - std::cout << "Axon Transfer - S3 transfer daemon - Design pending\n"; - return 0; + std::cout << "Axon Transfer - S3 transfer daemon - Design pending\n"; + return 0; } diff --git a/middlewares/mock/src/mock_plugin/include/mock_plugin.hpp b/middlewares/mock/src/mock_plugin/include/mock_plugin.hpp index a60846d..7c6f4c6 100644 --- a/middlewares/mock/src/mock_plugin/include/mock_plugin.hpp +++ b/middlewares/mock/src/mock_plugin/include/mock_plugin.hpp @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2026 ArcheBase + * + * SPDX-License-Identifier: MulanPSL-2.0 + */ + #ifndef MOCK_PLUGIN_HPP #define MOCK_PLUGIN_HPP diff --git a/middlewares/mock/src/mock_plugin/src/mock_plugin.cpp b/middlewares/mock/src/mock_plugin/src/mock_plugin.cpp index bcc7a3b..b91b19b 100644 --- a/middlewares/mock/src/mock_plugin/src/mock_plugin.cpp +++ b/middlewares/mock/src/mock_plugin/src/mock_plugin.cpp @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2026 ArcheBase + * + * SPDX-License-Identifier: MulanPSL-2.0 + */ + #include "mock_plugin.hpp" #include diff --git a/middlewares/mock/src/mock_plugin/src/mock_plugin_export.cpp b/middlewares/mock/src/mock_plugin/src/mock_plugin_export.cpp index d5a702f..cc04925 100644 --- a/middlewares/mock/src/mock_plugin/src/mock_plugin_export.cpp +++ b/middlewares/mock/src/mock_plugin/src/mock_plugin_export.cpp @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2026 ArcheBase + * + * SPDX-License-Identifier: MulanPSL-2.0 + */ + // Mock Plugin C ABI Export // Implements the Axon plugin C ABI interface for testing @@ -196,10 +202,12 @@ struct AxonPluginDescriptor { // Static vtable static AxonPluginVtable mock_vtable = { - axon_init, axon_start, axon_stop, axon_subscribe, axon_publish, {nullptr}}; + axon_init, axon_start, axon_stop, axon_subscribe, axon_publish, {nullptr} +}; // Exported plugin descriptor -__attribute__((visibility("default"))) const AxonPluginDescriptor* axon_get_plugin_descriptor(void +__attribute__((visibility("default"))) const AxonPluginDescriptor* axon_get_plugin_descriptor( + void ) { static const AxonPluginDescriptor descriptor = { AXON_ABI_VERSION_MAJOR, @@ -208,7 +216,8 @@ __attribute__((visibility("default"))) const AxonPluginDescriptor* axon_get_plug "1.0.0", "1.0.0", &mock_vtable, - {nullptr}}; + {nullptr} + }; return &descriptor; } diff --git a/middlewares/mock/src/mock_plugin/test/test_mock_plugin_e2e.cpp b/middlewares/mock/src/mock_plugin/test/test_mock_plugin_e2e.cpp index f59af13..f1f9575 100644 --- a/middlewares/mock/src/mock_plugin/test/test_mock_plugin_e2e.cpp +++ b/middlewares/mock/src/mock_plugin/test/test_mock_plugin_e2e.cpp @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2026 ArcheBase + * + * SPDX-License-Identifier: MulanPSL-2.0 + */ + /** * @file test_mock_plugin_e2e.cpp * @brief End-to-end test for the mock plugin without PluginLoader diff --git a/middlewares/mock/src/mock_plugin/test/test_mock_plugin_load.cpp b/middlewares/mock/src/mock_plugin/test/test_mock_plugin_load.cpp index 26e8f11..03bd309 100644 --- a/middlewares/mock/src/mock_plugin/test/test_mock_plugin_load.cpp +++ b/middlewares/mock/src/mock_plugin/test/test_mock_plugin_load.cpp @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2026 ArcheBase + * + * SPDX-License-Identifier: MulanPSL-2.0 + */ + /** * @file test_mock_plugin_load.cpp * @brief Test loading the mock plugin using PluginLoader @@ -122,7 +128,8 @@ int main(int argc, char* argv[]) { std::cout << std::endl; }; - if (descriptor->vtable->subscribe("/test_topic", "std_msgs/String", callback, &message_count) != 0) { + if (descriptor->vtable->subscribe("/test_topic", "std_msgs/String", callback, &message_count) != + 0) { std::cerr << "[FAIL] Could not subscribe to topic" << std::endl; return 1; } From 421ab92e7bff4c1af23dc84f5c8288f0be898058 Mon Sep 17 00:00:00 2001 From: Liangwei XU Date: Tue, 10 Feb 2026 21:49:19 +0800 Subject: [PATCH 07/10] feat: update mock plugin ABI and add task config callback --- CLAUDE.md | 66 +++++++++++++++---- apps/axon_recorder/http_server.cpp | 1 + apps/axon_recorder/http_server.hpp | 7 ++ apps/axon_recorder/recorder.cpp | 4 ++ docker/scripts/run_e2e_tests.sh | 22 ++++++- .../mock_plugin/src/mock_plugin_export.cpp | 7 +- .../test/test_mock_plugin_load.cpp | 5 +- 7 files changed, 94 insertions(+), 18 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0d7748c..3f065bd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -235,22 +235,29 @@ Axon/ ├── middlewares/ # Middleware-specific plugins and filters │ ├── ros1/ # ROS1 (Noetic) plugin → libaxon_ros1.so │ ├── ros2/ # ROS2 (Humble/Jazzy/Rolling) plugin → libaxon_ros2.so -│ │ └── src/ros2_plugin/ # ROS2 plugin implementation │ ├── zenoh/ # Zenoh plugin → libaxon_zenoh.so +│ ├── mock/ # Mock plugin for testing (no ROS required) +│ │ └── src/mock_plugin/ # Mock plugin implementation │ └── filters/ # Data processing filters (shared across plugins) -│ └── depthlitez/ # Depth image compression library +│ ├── include/ # Depth compressor header +│ ├── src/ # Depth compressor implementation +│ └── depthlitez/ # DepthLiteZ library (private submodule) │ ├── apps/ # Main applications │ ├── axon_recorder/ # Plugin loader and HTTP RPC server -│ └── plugin_example/ # Example plugin implementation +│ ├── axon_panel/ # Vue 3 web control panel +│ ├── axon_config/ # Robot configuration CLI tool (placeholder) +│ └── axon_transfer/ # S3 transfer daemon (placeholder) │ -├── tools/ # Utility tools and web applications -│ └── axon_panel/ # Vue 3 web control panel +├── python/ # Python client library +│ └── axon_client/ # Async/sync HTTP client │ └── docs/designs/ # Design documents ├── rpc-api-design.md # HTTP RPC API specification ├── frontend-design.md # AxonPanel web UI architecture - └── depth-compression-filter.md # Depth compression design + ├── middleware-plugin-architecture-design.md # Plugin architecture + ├── license-management-design.md # REUSE licensing + └── depth-compression-filter.md # Depth compression design ``` **Plugin ABI Interface:** @@ -267,6 +274,13 @@ The plugin interface is defined in [apps/axon_recorder/plugin_loader.hpp](apps/a 5. Middleware-specific bugs are isolated to plugin code 6. Filters in `middlewares/filters/` can be shared across plugins +**Mock Plugin for Testing:** +The mock plugin ([middlewares/mock/src/mock_plugin/](middlewares/mock/src/mock_plugin/)) provides a reference implementation for E2E testing without ROS dependencies: +- Simulates message publishing and subscription +- Implements the full plugin C ABI interface +- Enables CI testing without requiring ROS installation +- Test scripts: [test_e2e_with_mock.sh](middlewares/mock/test_e2e_with_mock.sh), [test_full_workflow.sh](middlewares/mock/test_full_workflow.sh) + ### State Machine ``` @@ -550,6 +564,34 @@ Keep descriptions under 72 characters. Use imperative mood ("add" not "added"). **Note**: With the new plugin architecture, most ROS-specific code is isolated within `middlewares/ros1/` and `middlewares/ros2/` plugins. Core code in `core/` and `apps/` should remain middleware-agnostic. +### License Management (REUSE) + +This project uses [REUSE](https://reuse.software/) for license compliance. All source files must include SPDX headers: + +```c +/* + * SPDX-FileCopyrightText: 2026 ArcheBase + * + * SPDX-License-Identifier: MulanPSL-2.0 + */ +``` + +**Adding licenses to new files:** +```bash +# Auto-add headers to C/C++ files +reuse annotate --year 2026 --copyright "ArcheBase" --license "MulanPSL-2.0" --style c + +# Check compliance +reuse lint +``` + +**Project-wide rules in [REUSE.toml](REUSE.toml):** +- Frontend assets (`apps/axon_panel/**`) are covered by a single annotation +- Mock files follow the pattern `**/*_mock.*` +- Dependencies and build artifacts are excluded + +**Note**: With the new plugin architecture, most ROS-specific code is isolated within `middlewares/ros1/` and `middlewares/ros2/` plugins. Core code in `core/` and `apps/` should remain middleware-agnostic. + ## Refactoring Guidelines **When refactoring code in this codebase, follow these principles:** @@ -706,11 +748,12 @@ grep -r "AXON_ROS1\|AXON_ROS2" build/ | Purpose | Location | |---------|----------| | Core libraries | `core/axon_*/` | -| ROS1 plugin | `middlewares/ros1/src/ros1_plugin/` (CMake) | -| ROS2 plugin | `middlewares/ros2/src/ros2_plugin/` (CMake) | -| Zenoh plugin | `middlewares/zenoh/` (CMake) | +| ROS1 plugin | `middlewares/ros1/` | +| ROS2 plugin | `middlewares/ros2/` | +| Zenoh plugin | `middlewares/zenoh/` | +| Mock plugin (testing) | `middlewares/mock/src/mock_plugin/` | | Filters | `middlewares/filters/` (shared data processing) | -| Depth compression | `middlewares/filters/depthlitez/` | +| Depth compression | `middlewares/filters/depthlitez/` (private) | | Recorder app (HTTP RPC) | `apps/axon_recorder/` | | Transfer daemon | `apps/axon_transfer/` | | Web control panel | `apps/axon_panel/` (Vue 3) | @@ -719,6 +762,7 @@ grep -r "AXON_ROS1\|AXON_ROS2" build/ | Tests | `*/test/` or `*/test_*.cpp` | | CMake modules | `cmake/` | | Design docs | `docs/designs/` | +| Python client | `python/axon_client/` | ### Plugin Development @@ -727,7 +771,7 @@ To create a new middleware plugin: 1. **Define the plugin ABI** - Implement the C interface in [apps/axon_recorder/plugin_loader.hpp](apps/axon_recorder/plugin_loader.hpp) 2. **Export descriptor function** - Each plugin must export `axon_get_plugin_descriptor()` 3. **Compile as shared library** - Build as `.so` with C linkage for ABI functions -4. **Example reference** - See [apps/plugin_example/](apps/plugin_example/) and [middlewares/ros2/src/ros2_plugin/](middlewares/ros2/src/ros2_plugin/) +4. **Example reference** - See [middlewares/mock/src/mock_plugin/](middlewares/mock/src/mock_plugin/) for a minimal plugin, or [middlewares/ros2/](middlewares/ros2/) for a full ROS2 implementation **ABI Versioning:** - `AxonPluginDescriptor` contains `abi_version_major` and `abi_version_minor` diff --git a/apps/axon_recorder/http_server.cpp b/apps/axon_recorder/http_server.cpp index ccdc12b..0e9d0fa 100644 --- a/apps/axon_recorder/http_server.cpp +++ b/apps/axon_recorder/http_server.cpp @@ -15,6 +15,7 @@ #include #include +#include "task_config.hpp" #include "version.hpp" // Logging infrastructure diff --git a/apps/axon_recorder/http_server.hpp b/apps/axon_recorder/http_server.hpp index 88cf048..1479687 100644 --- a/apps/axon_recorder/http_server.hpp +++ b/apps/axon_recorder/http_server.hpp @@ -20,6 +20,11 @@ namespace axon { namespace recorder { +/** + * Forward declaration + */ +struct TaskConfig; + /** * HTTP RPC Server for AxonRecorder * @@ -40,6 +45,7 @@ class HttpServer { using PauseRecordingCallback = std::function; using ResumeRecordingCallback = std::function; using ClearConfigCallback = std::function; + using GetTaskConfigCallback = std::function; using QuitCallback = std::function; /** @@ -74,6 +80,7 @@ class HttpServer { PauseRecordingCallback pause_recording; ResumeRecordingCallback resume_recording; ClearConfigCallback clear_config; + GetTaskConfigCallback get_task_config; QuitCallback quit; }; diff --git a/apps/axon_recorder/recorder.cpp b/apps/axon_recorder/recorder.cpp index 2265b5b..bc5c89a 100644 --- a/apps/axon_recorder/recorder.cpp +++ b/apps/axon_recorder/recorder.cpp @@ -440,6 +440,10 @@ bool AxonRecorder::start_http_server(const std::string& host, uint16_t port) { return this->transition_to(RecorderState::IDLE, error_msg); }; + callbacks.get_task_config = [this]() -> const TaskConfig* { + return this->get_task_config(); + }; + callbacks.quit = [this]() -> void { this->request_shutdown(); }; diff --git a/docker/scripts/run_e2e_tests.sh b/docker/scripts/run_e2e_tests.sh index e04122e..f552b8b 100755 --- a/docker/scripts/run_e2e_tests.sh +++ b/docker/scripts/run_e2e_tests.sh @@ -201,6 +201,23 @@ else } fi +# Build the mock plugin separately (it has its own CMakeLists.txt) +echo "" +echo "Building mock plugin..." +MOCK_PLUGIN_BUILD_DIR="${WORKSPACE_ROOT}/middlewares/mock/src/mock_plugin/build" +mkdir -p "${MOCK_PLUGIN_BUILD_DIR}" +cd "${MOCK_PLUGIN_BUILD_DIR}" + +cmake -DCMAKE_BUILD_TYPE=Release .. || { + echo "ERROR: Failed to configure mock plugin" + exit 1 +} + +cmake --build . -j$(nproc) || { + echo "ERROR: Failed to build mock plugin" + exit 1 +} + # Re-source workspace after build ros_workspace_source_workspace "${WORKSPACE_ROOT}" || { ros_workspace_error "Failed to source workspace after build" @@ -218,11 +235,10 @@ if [ ! -f "$RECORDER_BIN" ]; then fi echo "✓ axon_recorder binary found at ${RECORDER_BIN}" -# Check mock plugin -MOCK_PLUGIN="${WORKSPACE_ROOT}/build/middlewares/axon_mock.so" +# Check mock plugin - it's built in its own build directory +MOCK_PLUGIN="${MOCK_PLUGIN_BUILD_DIR}/libmock_plugin.so" if [ ! -f "$MOCK_PLUGIN" ]; then echo "ERROR: Mock plugin not found at ${MOCK_PLUGIN}" - echo "This should have been built with AXON_BUILD_MOCK_PLUGIN=ON" exit 1 fi echo "✓ Mock plugin found at ${MOCK_PLUGIN}" diff --git a/middlewares/mock/src/mock_plugin/src/mock_plugin_export.cpp b/middlewares/mock/src/mock_plugin/src/mock_plugin_export.cpp index cc04925..c43220a 100644 --- a/middlewares/mock/src/mock_plugin/src/mock_plugin_export.cpp +++ b/middlewares/mock/src/mock_plugin/src/mock_plugin_export.cpp @@ -115,8 +115,11 @@ static int32_t axon_stop(void) { // Subscribe to a topic with callback static int32_t axon_subscribe( - const char* topic_name, const char* message_type, AxonMessageCallback callback, void* user_data + const char* topic_name, const char* message_type, const char* options_json, + AxonMessageCallback callback, void* user_data ) { + (void)options_json; // Ignore options for mock plugin + if (!topic_name || !message_type || !callback) { return static_cast(AXON_ERROR_INVALID_ARGUMENT); } @@ -184,7 +187,7 @@ struct AxonPluginVtable { int32_t (*init)(const char*); int32_t (*start)(void); int32_t (*stop)(void); - int32_t (*subscribe)(const char*, const char*, AxonMessageCallback, void*); + int32_t (*subscribe)(const char*, const char*, const char*, AxonMessageCallback, void*); int32_t (*publish)(const char*, const uint8_t*, size_t, const char*); void* reserved[9]; }; diff --git a/middlewares/mock/src/mock_plugin/test/test_mock_plugin_load.cpp b/middlewares/mock/src/mock_plugin/test/test_mock_plugin_load.cpp index 03bd309..9ca96e1 100644 --- a/middlewares/mock/src/mock_plugin/test/test_mock_plugin_load.cpp +++ b/middlewares/mock/src/mock_plugin/test/test_mock_plugin_load.cpp @@ -128,8 +128,9 @@ int main(int argc, char* argv[]) { std::cout << std::endl; }; - if (descriptor->vtable->subscribe("/test_topic", "std_msgs/String", callback, &message_count) != - 0) { + if (descriptor->vtable->subscribe( + "/test_topic", "std_msgs/String", "{}", callback, &message_count + ) != 0) { std::cerr << "[FAIL] Could not subscribe to topic" << std::endl; return 1; } From 6e90ed1f2608a6f2f2ba6c5d0d0429c2dc955fde Mon Sep 17 00:00:00 2001 From: Liangwei XU Date: Tue, 10 Feb 2026 22:07:13 +0800 Subject: [PATCH 08/10] feat: add project config and optimize API logging --- apps/axon_panel/src/api/rpc.js | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/apps/axon_panel/src/api/rpc.js b/apps/axon_panel/src/api/rpc.js index 6183334..264a8bd 100644 --- a/apps/axon_panel/src/api/rpc.js +++ b/apps/axon_panel/src/api/rpc.js @@ -13,11 +13,15 @@ const apiClient = axios.create({ // Request interceptor apiClient.interceptors.request.use( (config) => { - console.log('[API Request]', config.method.toUpperCase(), config.url, config.data) + if (import.meta.env.DEV) { + console.log('[API Request]', config.method.toUpperCase(), config.url, config.data) + } return config }, (error) => { - console.error('[API Request Error]', error) + if (import.meta.env.DEV) { + console.error('[API Request Error]', error) + } return Promise.reject(error) } ) @@ -25,13 +29,17 @@ apiClient.interceptors.request.use( // Response interceptor apiClient.interceptors.response.use( (response) => { - console.log('[API Response]', response.config.url, response.data) + if (import.meta.env.DEV) { + console.log('[API Response]', response.config.url, response.data) + } return response }, (error) => { - console.error('[API Response Error]', error.config?.url, error.message) - if (error.response) { - console.error('[API Error Response]', error.response.data) + if (import.meta.env.DEV) { + console.error('[API Response Error]', error.config?.url, error.message) + if (error.response) { + console.error('[API Error Response]', error.response.data) + } } return Promise.reject(error) } From 35a783e6c0aefd834660e09bb5486e5c52fb399e Mon Sep 17 00:00:00 2001 From: Liangwei XU Date: Tue, 10 Feb 2026 22:09:14 +0800 Subject: [PATCH 09/10] feat: add project config options and remove vite port --- apps/axon_panel/vite.config.js | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/axon_panel/vite.config.js b/apps/axon_panel/vite.config.js index 5dc9fbd..562c11e 100644 --- a/apps/axon_panel/vite.config.js +++ b/apps/axon_panel/vite.config.js @@ -4,7 +4,6 @@ import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], server: { - port: 3000, host: '0.0.0.0', proxy: { '/rpc': { From f177d173b4e8b3cd33cc36904aae54914a9cd06b Mon Sep 17 00:00:00 2001 From: Liangwei XU Date: Tue, 10 Feb 2026 22:13:10 +0800 Subject: [PATCH 10/10] refactor: simplify mock plugin path in e2e tests --- apps/axon_recorder/test/e2e/run_e2e_tests.sh | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/apps/axon_recorder/test/e2e/run_e2e_tests.sh b/apps/axon_recorder/test/e2e/run_e2e_tests.sh index 78e0867..167f70f 100755 --- a/apps/axon_recorder/test/e2e/run_e2e_tests.sh +++ b/apps/axon_recorder/test/e2e/run_e2e_tests.sh @@ -32,19 +32,8 @@ else RECORDER_BIN="${BUILD_DIR}/axon_recorder/axon_recorder" fi -# Mock plugin path - use the newly created mock middleware +# Mock plugin path - built in-place by mock middleware's own build system MOCK_PLUGIN="${PROJECT_ROOT}/middlewares/mock/src/mock_plugin/build/libmock_plugin.so" -if [[ ! -f "${MOCK_PLUGIN}" ]]; then - # Fallback to build directory - MOCK_PLUGIN="${BUILD_DIR}/middlewares/mock/src/mock_plugin/build/libmock_plugin.so" -fi -if [[ ! -f "${MOCK_PLUGIN}" ]]; then - # Another fallback for backward compatibility - MOCK_PLUGIN="${BUILD_DIR}/middlewares/axon_mock.so" -fi -if [[ ! -f "${MOCK_PLUGIN}" ]]; then - MOCK_PLUGIN="${PROJECT_ROOT}/apps/axon_recorder/build/axon_mock.so" -fi # Colors for output RED='\033[0;31m' @@ -121,8 +110,8 @@ setup() { if [[ ! -f "${MOCK_PLUGIN}" ]]; then log_warn "Mock plugin not found at ${MOCK_PLUGIN}" log_info "E2E tests will attempt to run without mock middleware" - log_info "Build mock middleware with: make build-mock" - log_info "Or from project root: cd middlewares/mock/src/mock_plugin/build && cmake .. && make" + log_info "Build mock middleware with:" + log_info " cd middlewares/mock/src/mock_plugin/build && cmake .. && make" MOCK_PLUGIN="" fi