diff --git a/README.md b/README.md index 1d696cf..45d0b82 100644 --- a/README.md +++ b/README.md @@ -357,6 +357,9 @@ colcon test # Run specific package tests colcon test --packages-select agent composer core + +#Running a specific test method +python3 -m pytest src/composer/test/test_muto_composer.py::TestMutoComposer::test_parse_payload_archive_format -v ``` ### Contributing diff --git a/docs/samples/april-tag-robot/README.md b/docs/samples/april-tag-robot/README.md new file mode 100644 index 0000000..d7f1474 --- /dev/null +++ b/docs/samples/april-tag-robot/README.md @@ -0,0 +1,333 @@ +# AprilTag Detection Sample + +AprilTags example + +Real-time AprilTag detection and visualization pipeline demonstrating computer vision workflows with Eclipse Muto orchestration and Eclipse Symphony fleet management. + +## Overview + +This sample implements a complete AprilTag detection system using: +- **Camera input**: Live camera feed or recorded bag files +- **Detection**: Fast AprilTag family detection (tag36h11, tagStandard41h12, etc.) +- **Visualization**: Optional detection overlay and tracking display +- **Orchestration**: Muto-managed composable node containers +- **Fleet Management**: Symphony-based remote deployment and updates + +### Key Features + +- **Lightweight perception**: Focus on detection without full 6-DoF pose estimation +- **Composable architecture**: Efficient memory sharing between vision nodes +- **Multi-family support**: Configurable tag families and detection parameters +- **Remote deployment**: Over-the-air updates via Symphony orchestration +- **Telemetry ready**: Detection metrics and performance monitoring + +> **Note**: This sample uses [apriltag_detector](https://github.com/ros-misc-utilities/apriltag_detector) for pure detection. For combined detection + pose estimation, consider [apriltag_ros](https://github.com/christianrauch/apriltag_ros). + +## Artifact Map + +Directory: [`samples/april-tag-robot/`](./) + +| File | Purpose | +|------|---------| +| `apriltag-detector-muto-stack.json` | Declarative Muto stack (JSON) for detector + draw nodes (composable container) | +| `apriltag-tracking-solution.json` | Symphony Solution (wraps stack as base64 payload) | +| `apriltag-tracking-instance.json` | Symphony Instance binding Solution → Target | +| `apriltag_workspace/stack.json` | Alternative workspace/stack form (if using archive packaging) | +| `apriltag_workspace/launch/apriltag.launch.yaml` | Upstream style launch YAML (reference only) | +| `apriltag_workspace/config/tags_36h11.yaml` | Tag family parameter file | + +Target definition is auto‑registered by the running Muto Agent (see talker–listener sample). For a static reference, review [`../talker-listener/test-robot-debug-target.json`](../talker-listener/test-robot-debug-target.json). + +## Scenario Overview + +You operate a fleet of robots each with a monocular camera. You need to: +1. Deploy an initial AprilTag detection capability (v1). +2. Safely roll out an improved pipeline (v2) that supports additional tag families and optional tracking/visualization. +3. Use canary rollout + rollback policies to limit risk. + +The complete deep‑dive OTA narrative (architecture + sequence diagrams) lives in `ros_variant/muto.md` (search for "AprilTag" section). This README keeps a concise, actionable version focused on running the sample. + +### Version Delta (Conceptual) +| Aspect | v1 (baseline) | v2 (enhanced) | +|--------|---------------|---------------| +| Families | `tag36h11` | `tag36h11`, `tagStandard41h12` | +| Nodes | detector, draw | detector, draw, (optional future tracker) | +| Telemetry | raw detections | detections + extended metrics | +| Policy | none | optional: only when docked | + +Refer to the architecture + sequence diagrams in `muto.md` for full context. Keeping this README lean. + +## Prerequisites + +- **Camera hardware**: USB camera, Intel RealSense, or other ROS 2 compatible camera +- **AprilTag packages**: Install detection dependencies + ```bash + sudo apt install ros-$ROS_DISTRO-apriltag ros-$ROS_DISTRO-apriltag-msgs + # Or build from source: https://github.com/ros-misc-utilities/apriltag_detector + ``` +- **Eclipse Muto + Symphony**: Complete setup from [Quick Start Guide](../../muto-quickstart.md) + +## Manual Testing (Native ROS 2) + +Before orchestrated deployment, verify the detection pipeline: + +```bash +# Terminal 1: Start camera (adjust topic as needed) +ros2 run usb_cam usb_cam_node_exe + +# Terminal 2: Start AprilTag detector +ros2 run apriltag_detector apriltag_detector_node --ros-args \ + -r /image:=/image_raw \ + -p tag_family:=tag36h11 + +# Terminal 3: View detections +ros2 topic echo /apriltag/detections +``` + +## Deploy the Sample + +### Step 1: Start Symphony + Muto +Follow the [Quick Start Guide](../../muto-quickstart.md) to get Muto + Symphony running. Ensure a target like `test-robot-debug` appears in Symphony: + +```bash +# Verify Symphony API is running +curl http://localhost:8082/v1alpha2/greetings + +# Check if Muto agent registered the target +curl -s http://localhost:8082/v1alpha2/targets | jq '.items[].metadata.name' +``` + +### Step 2: Review Stack Configuration +The AprilTag stack definition [`apriltag-detector-muto-stack.json`](./apriltag-detector-muto-stack.json) defines a composable container with two nodes: + +- **apriltag_detector_node**: Detects AprilTags in camera images +- **apriltag_draw_node**: Draws detection overlays on images + +Key configuration areas: +```json +{ + "composable": [ + { + "container_name": "apriltag_container", + "node": [ + { + "package": "apriltag_detector", + "executable": "apriltag_detector_node", + "parameters": [ + {"tag_family": "tag36h11"}, + {"tag_edge_size": 0.05} + ], + "remappings": [ + {"from": "/image", "to": "/camera/image_raw"} + ] + } + ] + } + ] +} +``` + +Edit parameters as needed: +- `tag_family`: Supported families include `tag36h11`, `tagStandard41h12` +- `tag_edge_size`: Physical tag size in meters +- Image topic remapping to match your camera driver + +### Step 3: Deploy Solution +Navigate to the samples directory and create the Solution: + +```bash +cd samples/ +./define-solution.sh april-tag-robot/apriltag-tracking-solution.json +``` + +The Solution wraps the stack JSON as a base64-encoded payload. If you modified the stack definition, regenerate the encoding: + +```bash +cd april-tag-robot/ +STACK=apriltag-detector-muto-stack.json +BASE64=$(cat "$STACK" | base64 -w0) +jq --arg d "$BASE64" '.spec.components[0].properties.data=$d' apriltag-tracking-solution.json > /tmp/new-solution.json +mv /tmp/new-solution.json apriltag-tracking-solution.json +``` + +### Step 4: Deploy Instance +Create the Instance to bind the Solution to your target: + +```bash +./define-instance.sh april-tag-robot/apriltag-tracking-instance.json +``` + +### Step 5: Verify Deployment +**ROS 2 Environment** (in container or host with ROS setup): +```bash +# Check AprilTag topics are available +ros2 topic list | grep apriltag + +# Verify detection output +ros2 topic echo /apriltag/detections --once + +# Monitor container status +ros2 node list | grep apriltag +``` + +**Symphony Environment**: +```bash +# Check instance status +curl -s http://localhost:8082/v1alpha2/instances | \ + jq '.items[] | select(.metadata.name|test("apriltag")) | {name: .metadata.name, phase: .status.phase}' + +# View deployment details +curl -s http://localhost:8082/v1alpha2/instances/apriltag-tracking-instance | jq '.status' +``` + +### Step 6: Update and Rollout (Optional) +To demonstrate OTA updates, create a modified stack (e.g., add a second tag family): + +1. Edit `apriltag-detector-muto-stack.json` to add another detector node +2. Create a new Solution version (`apriltag-tracking-solution-v2.json`) +3. Submit as a canary update: + +```bash +# Create solution v2 +./define-solution.sh april-tag-robot/apriltag-tracking-solution-v2.json + +# Deploy to specific target (canary) +./define-instance.sh april-tag-robot/apriltag-tracking-instance-v2.json +``` + +## Monitoring and Telemetry + +### Key Topics +Monitor these ROS 2 topics to verify AprilTag detection: + +| Topic | Message Type | Purpose | +|-------|--------------|---------| +| `/apriltag/detections` | `apriltag_msgs/AprilTagDetectionArray` | Core detection results with tag IDs and poses | +| `/apriltag/images/debug` | `sensor_msgs/Image` | Visualization with detection overlays (if enabled) | +| `/camera/image_raw` | `sensor_msgs/Image` | Input camera stream | +| `/apriltag/detections/compressed` | `CompressedImage` | Compressed debug visualization | + +### Sample Detection Output +```bash +ros2 topic echo /apriltag/detections --once +``` +Expected structure: +```yaml +detections: +- id: 1 + family: tag36h11 + center: {x: 320.5, y: 240.2} + corners: [{x: 315.0, y: 235.0}, {x: 326.0, y: 235.0}, ...] + pose: + position: {x: 0.12, y: -0.05, z: 0.8} + orientation: {x: 0.0, y: 0.0, z: 0.707, w: 0.707} +``` + +## Troubleshooting + +### Common Issues + +**No detections appearing**: +```bash +# Check if camera is publishing +ros2 topic hz /camera/image_raw + +# Verify detector node is running +ros2 node list | grep apriltag + +# Check parameter configuration +ros2 param get /apriltag_container/apriltag_detector_node tag_family +``` + +**Deployment failing**: +```bash +# Check Symphony instance status +curl -s http://localhost:8082/v1alpha2/instances/apriltag-tracking-instance | jq '.status.observedState' + +# View Muto agent logs +ros2 topic echo /muto/logs --once + +# Check container health +docker ps | grep apriltag # or podman ps +``` + +**Performance issues**: +- Reduce image resolution in camera driver +- Adjust `tag_decimate` parameter (trade accuracy for speed) +- Verify adequate CPU resources for container + +### Validation Checklist + +| Step | Command | Expected Result | +|------|---------|-----------------| +| Symphony API available | `curl http://localhost:8082/v1alpha2/greetings` | "Hello from Symphony..." | +| Target registered | `curl -s http://localhost:8082/v1alpha2/targets \| jq '.[].metadata.name'` | Shows target name | +| Instance deployed | `curl -s http://localhost:8082/v1alpha2/instances \| grep apriltag` | Instance exists | +| Camera publishing | `ros2 topic hz /camera/image_raw` | Shows frame rate | +| Detector active | `ros2 node list \| grep apriltag` | Detector node listed | +| Detections streaming | `ros2 topic echo /apriltag/detections --once` | Detection data appears | + +## Advanced Configuration + +### Update Policies +Configure deployment policies in Symphony for production scenarios: + +```json +{ + "updatePolicy": { + "canary": { + "percentage": 10, + "conditions": ["battery_state=docked", "cpu_usage<80%"] + }, + "rollback": { + "triggers": ["detection_fps<5", "error_rate>0.1"] + } + } +} +``` + +### Multi-Camera Setup +For robots with multiple cameras, create separate detector instances: + +```json +{ + "node": [ + { + "package": "apriltag_detector", + "executable": "apriltag_detector_node", + "name": "front_camera_detector", + "remappings": [{"from": "/image", "to": "/front_camera/image_raw"}] + }, + { + "package": "apriltag_detector", + "executable": "apriltag_detector_node", + "name": "rear_camera_detector", + "remappings": [{"from": "/image", "to": "/rear_camera/image_raw"}] + } + ] +} +``` + +## Next Steps + +### Extensions +- **Depth Integration**: Add pose refinement using depth cameras +- **Tracking**: Implement persistent tag tracking across frames +- **Fleet Coordination**: Share tag detections between robots via DDS +- **Performance**: GPU-accelerated detection for real-time applications + +### Production Considerations +- Configure signed container images for security compliance +- Implement proper tag size calibration procedures +- Add detection confidence thresholds and filtering +- Set up centralized logging and metrics collection + +## References +- [AprilTag Detector ROS 2 Package](https://github.com/ros-misc-utilities/apriltag_detector) +- [AprilTag Library Documentation](https://april.eecs.umich.edu/software/apriltag) +- [Symphony OTA Updates Guide](https://github.com/eclipse-symphony/symphony/blob/main/docs/README.md) +- [Muto Architecture Documentation](../../README.md) + +--- +This sample demonstrates basic AprilTag detection in a fleet management context. Build upon this foundation with tracking, multi-sensor fusion, and advanced deployment policies as your use case evolves. + diff --git a/docs/samples/muto/sample-composable.json b/docs/samples/muto/sample-composable.json new file mode 100644 index 0000000..bb28e2b --- /dev/null +++ b/docs/samples/muto/sample-composable.json @@ -0,0 +1,26 @@ +{ + "name": "Muto Simple Composable Client-Server Stack", + "context": "eteration_office", + "stackId": "org.eclipse.muto.sandbox:composable_client_server", + "composable": [ + { + "name": "muto_demo_container", + "namespace": "", + "package": "rclcpp_components", + "executable": "component_container", + "node": [ + { + "pkg": "composition", + "plugin": "composition::Server", + "name": "server" + }, + { + "pkg": "composition", + "plugin": "composition::Client", + "name": "client" + } + ] + } + ], + "node": [] +} \ No newline at end of file diff --git a/docs/samples/muto/sample-muto-stack.json b/docs/samples/muto/sample-muto-stack.json new file mode 100644 index 0000000..7f68cad --- /dev/null +++ b/docs/samples/muto/sample-muto-stack.json @@ -0,0 +1,91 @@ +{ + "name": "F1tenth Multiagent Gym", + "context": "eteration_office", + "stackId": "org.eclipse.muto.sandbox:f1tenth-multiagent-gym.launch", + "stack": [ + { + "thingId": "org.eclipse.muto.sandbox:racecar1.launch" + }, + { + "thingId": "org.eclipse.muto.sandbox:racecar2.launch" + }, + { + "thingId": "org.eclipse.muto.sandbox:racecar3.launch" + } + ], + "arg": [ + { + "name": "map", + "value": "$(find f1tenth_gym_ros)/maps/Spielberg_map.yaml" + } + ], + "param": [], + "node": [ + { + "name": "map_server", + "pkg": "nav2_map_server", + "exec": "map_server", + "param": [ + { + "name": "yaml_filename", + "value": "$(arg map)" + }, + { + "name": "topic", + "value": "map" + }, + { + "name": "frame_id", + "value": "map" + }, + { + "name": "output", + "value": "screen" + }, + { + "name": "use_sim_time", + "value": "True" + } + ] + }, + { + "name": "gym_bridge", + "pkg": "f1tenth_gym_ros", + "exec": "gym_bridge", + "param": [ + { + "from": "$(find f1tenth_gym_ros)/config/sim.yaml" + } + ] + }, + { + "pkg": "rviz2", + "name": "rviz", + "exec": "rviz2", + "args": "-d $(find f1tenth_gym_ros)/launch/gym_bridge.rviz", + "output": "screen" + }, + { + "pkg": "nav2_lifecycle_manager", + "exec": "lifecycle_manager", + "name": "lifecycle_manager_localization", + "output": "screen", + "param": [ + { + "name": "use_sim_time", + "value": "True" + }, + { + "name": "autostart", + "value": "True" + }, + { + "name": "node_names", + "value": [ + "map_server" + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/docs/samples/symphony/define-instance.sh b/docs/samples/symphony/define-instance.sh index 4d216fc..a7adc6c 100755 --- a/docs/samples/symphony/define-instance.sh +++ b/docs/samples/symphony/define-instance.sh @@ -1,5 +1,25 @@ #!/bin/bash + +# Check if JSON file argument is provided +if [ $# -eq 0 ]; then + echo "Usage: $0 " + echo "Example: $0 apriltag-tracking-instance.json" + exit 1 +fi + +JSON_FILE="$1" + +# Check if file exists +if [ ! -f "$JSON_FILE" ]; then + echo "Error: File '$JSON_FILE' not found!" + exit 1 +fi + +# Extract solution name from filename (remove path and .json extension) +ROOT_NAME=$(basename "$JSON_FILE" .json) +INSTANCE_NAME="${ROOT_NAME}" + export SYMPHONY_API_URL=http://localhost:8082/v1alpha2/ TOKEN=$(curl -X POST -H "Content-Type: application/json" -d '{"username":"admin","password":""}' "${SYMPHONY_API_URL}users/auth" | jq -r '.accessToken') @@ -7,11 +27,26 @@ TOKEN=$(curl -X POST -H "Content-Type: application/json" -d '{"username":"admin" # Prompt user to press Enter to continue after the target has been registered -curl -v -s -X GET -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" "${SYMPHONY_API_URL}instances" +curl -X GET -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" "${SYMPHONY_API_URL}instances" -# Read the content of solution.json file and send it as data in the POST request +# Read & mutate JSON: overwrite metadata.name with INSTANCE_NAME using jq SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -SOLUTION_DATA=$(cat "$SCRIPT_DIR/instance.json") - -curl -v -s -X POST -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" -d "$SOLUTION_DATA" "${SYMPHONY_API_URL}instances/test-robot-debug-instance" - +if ! command -v jq >/dev/null 2>&1; then + echo "Error: jq is required but not installed." >&2 + exit 2 +fi + +# Use --arg to safely inject shell variable +SOLUTION_DATA=$(jq --arg name "$INSTANCE_NAME" '(.metadata //= {}) | .metadata.name = $name' "$JSON_FILE") + + +HTTP_RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $TOKEN" \ + -d "$SOLUTION_DATA" \ + "${SYMPHONY_API_URL}instances/${INSTANCE_NAME}") +if [ "$HTTP_RESPONSE" -eq 200 ] || [ "$HTTP_RESPONSE" -eq 204 ]; then + echo "Instance '${INSTANCE_NAME}' created successfully." +else + echo "Failed to create instance '${INSTANCE_NAME}'. HTTP status: $HTTP_RESPONSE" +fi diff --git a/docs/samples/symphony/define-solution.sh b/docs/samples/symphony/define-solution.sh index 4f69936..0543bd1 100755 --- a/docs/samples/symphony/define-solution.sh +++ b/docs/samples/symphony/define-solution.sh @@ -58,9 +58,9 @@ encode_base64() { base64 -i "$file" 2>/dev/null || base64 < "$file" 2>/dev/null elif [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]] || [[ -n "$WSL_DISTRO_NAME" ]] || [[ -n "$IS_WSL" ]] || [[ "$(uname -r)" == *Microsoft* ]] || [[ "$(uname -r)" == *microsoft* ]]; then # WSL/Windows environments - try different approaches - if base64 -w 0 "$file" 2>/dev/null; then + if command -v base64 >/dev/null 2>&1 && base64 -w 0 "$file" >/dev/null 2>&1; then base64 -w 0 "$file" - elif base64 < "$file" 2>/dev/null; then + elif command -v base64 >/dev/null 2>&1 && base64 < "$file" >/dev/null 2>&1; then base64 < "$file" | tr -d '\n' else # Fallback for older WSL versions @@ -68,7 +68,7 @@ encode_base64() { fi else # Linux/Unix (GNU base64) - if base64 -w 0 "$file" 2>/dev/null; then + if command -v base64 >/dev/null 2>&1 && base64 -w 0 "$file" >/dev/null 2>&1; then base64 -w 0 "$file" else # Fallback for systems without -w option @@ -80,7 +80,6 @@ encode_base64() { # Base64 encode the contents of the JSON file echo "Encoding stack data to base64..." STACK_DATA_BASE64=$(encode_base64 "$JSON_FILE") - if [ -z "$STACK_DATA_BASE64" ]; then echo "Error: Failed to base64 encode the JSON file" exit 1 diff --git a/docs/samples/symphony/delete-instance.sh b/docs/samples/symphony/delete-instance.sh new file mode 100755 index 0000000..de64b8b --- /dev/null +++ b/docs/samples/symphony/delete-instance.sh @@ -0,0 +1,58 @@ +#!/bin/bash + + +# Check if JSON file argument is provided +if [ $# -eq 0 ]; then + echo "Usage: $0 " + echo "Example: $0 apriltag-tracking-instance.json" + exit 1 +fi + +JSON_FILE="$1" + +# Check if file exists +if [ ! -f "$JSON_FILE" ]; then + echo "Error: File '$JSON_FILE' not found!" + exit 1 +fi + +# Extract solution name from filename (remove path and .json extension) +ROOT_NAME=$(basename "$JSON_FILE" .json) +INSTANCE_NAME="${ROOT_NAME}" + +export SYMPHONY_API_URL=http://localhost:8082/v1alpha2/ + +TOKEN=$(curl -X POST -H "Content-Type: application/json" -d '{"username":"admin","password":""}' "${SYMPHONY_API_URL}users/auth" | jq -r '.accessToken') + + +# Prompt user to press Enter to continue after the target has been registered + +curl -X GET -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" "${SYMPHONY_API_URL}instances" + +# Read & mutate JSON: overwrite metadata.name with INSTANCE_NAME using jq +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if ! command -v jq >/dev/null 2>&1; then + echo "Error: jq is required but not installed." >&2 + exit 2 +fi + +# Use --arg to safely inject shell variable +SOLUTION_DATA=$(jq --arg name "$INSTANCE_NAME" '(.metadata //= {}) | .metadata.name = $name' "$JSON_FILE") + +# DELETE THE INSTANCE AND SHOW THE RESULT OF THE DELETE as SUCCESSFUL OR NOT + +echo "" +echo "---" +echo "Deleting instance with metadata.name=$INSTANCE_NAME" +HTTP_RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $TOKEN" \ + "${SYMPHONY_API_URL}instances/${INSTANCE_NAME}") + +if [ "$HTTP_RESPONSE" -eq 200 ] || [ "$HTTP_RESPONSE" -eq 204 ]; then + echo "Instance '${INSTANCE_NAME}' deleted successfully." +else + echo "Failed to delete instance '${INSTANCE_NAME}'. HTTP status: $HTTP_RESPONSE" + exit 3 +fi + diff --git a/docs/samples/symphony/docker-compose.yaml b/docs/samples/symphony/docker-compose.yaml new file mode 100644 index 0000000..965eea1 --- /dev/null +++ b/docs/samples/symphony/docker-compose.yaml @@ -0,0 +1,46 @@ + +services: + symphony-api: + image: ghcr.io/eclipse-symphony/symphony-api:0.48-proxy.40 + container_name: symphony-api + ports: + - "8082:8082" + environment: + - USE_SERVICE_ACCOUNT_TOKENS=false + - SYMPHONY_API_URL=http://localhost:8082/v1alpha2/ + - CONFIG=/symphony-api-no-k8s.json + - LOG_LEVEL=Error + volumes: + - ./providers:/extensions + restart: unless-stopped + symphony-portal: + image: ghcr.io/eclipse-symphony/symphony-portal:0.48-proxy.40 + container_name: symphony-portal + ports: + - "3000:3000" + environment: + - SYMPHONY_API=http://symphony-api:8082/v1alpha2/ + - NEXTAUTH_SECRET=SymphonyKey + - NEXTAUTH_URL=http://localhost:3000 + restart: unless-stopped + depends_on: + - symphony-api + mosquitto: + image: eclipse-mosquitto:2 + container_name: mosquitto + ports: + - "1883:1883" # MQTT (TCP) + # - "9001:9001" # (optional) WebSocket listener, enable in config first + volumes: + - ./mosquitto/mosquitto.conf:/mosquitto/config/mosquitto.conf:ro + - mosquitto-data:/mosquitto/data + - mosquitto-logs:/mosquitto/log + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "mosquitto_sub -h localhost -p 1883 -t '$$SYS/#' -C 1 -W 3 >/dev/null 2>&1 || exit 1"] + interval: 10s + timeout: 5s + retries: 5 +volumes: + mosquitto-data: + mosquitto-logs: \ No newline at end of file diff --git a/docs/samples/symphony/mosquitto/mosquitto.conf b/docs/samples/symphony/mosquitto/mosquitto.conf new file mode 100644 index 0000000..59f8023 --- /dev/null +++ b/docs/samples/symphony/mosquitto/mosquitto.conf @@ -0,0 +1,6 @@ +listener 1883 0.0.0.0 +allow_anonymous true + +# (optional) WebSocket listener if you also expose 9001: +# listener 9001 +# protocol websockets diff --git a/docs/samples/talker-listener/README.md b/docs/samples/talker-listener/README.md new file mode 100644 index 0000000..02b997e --- /dev/null +++ b/docs/samples/talker-listener/README.md @@ -0,0 +1,239 @@ +# Talker-Listener sample (Muto + Symphony) + +This folder contains a small ROS 2 talker/listener stack used as a working example for Eclipse Muto orchestration and Eclipse Symphony solution/instance management. + +This README describes: +- How to start the local Muto system (native or container/dev workflow) +- How to start Symphony services (docker compose) +- How to register a solution and create an instance using the helper scripts +- Two ways to represent a stack: plain JSON (`stack/json`) and a packaged archive (`stack/archive`) +- How to create a new stack/archive with `create_archive.sh` + +## Prerequisites + +- ROS 2 (Humble or later) and `colcon` if running Muto natively +- Docker and Docker Compose (or `docker compose`) for Symphony +- `curl`, `jq`, and `base64` available for the helper scripts + +For a complete system setup and additional details see: `../../muto-quickstart.md` and the repository `README.md`. + +## Quick path — start Muto (locally) + +If you already have a built workspace and want to run Muto locally (native ROS 2): + +1. Build and source your workspace (example): + + 1. Build + + source /opt/ros/humble/setup.bash + colcon build --symlink-install + + 2. Source the install overlay + + source install/setup.bash + +2. Launch the main Muto system (example args used in docs and tests): + + ```bash + ros2 launch launch/muto.launch.py \ + vehicle_namespace:=org.eclipse.muto.test \ + vehicle_name:=test-robot-debug \ + enable_symphony:=true \ + log_level:=INFO + ``` + +This will start agent/composer/plugins and — when enabled — the Symphony provider so the device can talk to the Symphony control plane. + +Notes +- If you prefer running inside the development container described in `docs/development-container.md`, follow that doc to enter the container and run the same `ros2 launch` command after building. +- Verify nodes with `ros2 node list | grep muto` and topics with `ros2 topic list` / `ros2 topic echo /chatter`. + +## Start Symphony (docker-compose) + +Symphony (API + Portal + Mosquitto) can be started from `docs/samples/symphony` using Docker Compose: + +1. From repository root: + + ```bash + cd docs/samples/symphony + docker compose up -d + ``` + +2. Verify services: + + ```bash + # Symphony API + curl http://localhost:8082/v1alpha2/greetings + + # Symphony Portal GUI + open http://localhost:3000 # or visit in your browser + + # MQTT broker + # mosquitto: tcp://localhost:1883 + ``` + +If you need to stop the services: + + ```bash + docker compose down + ``` + +Notes +- The `docker-compose.yaml` in `docs/samples/symphony` includes a ready-to-run Symphony API, Symphony Portal, and an Eclipse Mosquitto broker. + +## Automated demo with run-demo.sh + +For a streamlined end-to-end experience, use the `run-demo.sh` script in this folder. It automates the process of verifying Symphony availability, waiting for Muto nodes to be ready, defining the archive-based solution, and creating an instance. + +### Prerequisites for run-demo.sh + +- Symphony services must already be running (see "Start Symphony (docker-compose)" section). +- Muto must be launched (see "Quick path — start Muto (locally)" section). +- Required tools: `curl`, `jq`, `base64`, `ros2`. + +### How to run run-demo.sh + +From the repository root: + +```bash +cd docs/samples/talker-listener +./run-demo.sh +``` + +### What run-demo.sh does + +1. **Checks prerequisites**: Verifies that required commands (`curl`, `jq`, `base64`, `ros2`) are available. +2. **Waits for Symphony API**: Polls `http://localhost:8082/v1alpha2/greetings` until the API responds (up to 60 seconds). +3. **Waits for Muto nodes**: Checks `ros2 node list` for the presence of required Muto nodes (up to 60 seconds): + - `/muto/agent` + - `/muto/commands_plugin` + - `/muto/compose_plugin` + - `/muto/core_twin` + - `/muto/gateway` + - `/muto/launch_plugin` + - `/muto/muto_composer` + - `/muto/muto_symphony_provider` + - `/muto/provision_plugin` +4. **Defines the solution**: Uses `define-solution.sh` to post `talker-listener-xarchive.json` to Symphony. +5. **Creates the instance**: Uses `define-instance.sh` to create an instance with `talker-listener-xarchive-instance.json`. +6. **Provides next steps**: Prints commands to check Symphony Portal/API, verify ROS topics, and clean up. + +If any step fails (e.g., Symphony not running, Muto nodes not appearing), the script exits with an error message. + + +## Define a solution (stack) and create an instance + +The repo includes helper scripts in `docs/samples/symphony` that use the Symphony API to create solutions and instances: + +- `define-solution.sh` — posts a solution to Symphony. Usage: `./define-solution.sh ` +- `define-instance.sh` — creates an instance (binds a solution to a target). Usage: `./define-instance.sh ` +- `delete-instance.sh` — deletes an instance. Usage: `./delete-instance.sh ` + +Examples (recommended sequence): + +1. Start Symphony (see previous section) and ensure it responds on `http://localhost:8082`. + +2. Create a solution describing the stack. From `docs/samples/symphony` run one of the options below. + +Option A: Use the plain JSON stack (stack/json) + +- The talker/listener JSON stack: `../talker-listener-json.json` (or `../talker-listener/talker-listener-json.json` depending on where you execute the script). +- Example (run from `docs/samples/symphony`): + + ```bash + ./define-solution.sh ../talker-listener/talker-listener-json.json + ``` + +Option B: Use the packaged stack archive (stack/archive) + +- A stack archive JSON is a manifest that contains base64-encoded tar.gz data (content_type: `stack/archive`). Example file: `talker-listener-xarchive.json`. +- Example (run from `docs/samples/symphony`): + + ```bash + ./define-solution.sh ../talker-listener/talker-listener-xarchive.json + ``` + +Both approaches create a Symphony solution that can later be instantiated against a target. + +3. Create an instance to bind the solution to a registered target (robot). The sample instance files are in this folder: + +- `talker-listener-json-instance.json` — uses the JSON stack solution +- `talker-listener-xarchive-instance.json` — uses the archive-based solution + +Example (run from `docs/samples/symphony`): + + ```bash + ./define-instance.sh ../talker-listener/talker-listener-json-instance.json + ``` + +or for the archive-based solution: + + ```bash + ./define-instance.sh ../talker-listener/talker-listener-xarchive-instance.json + ``` + +After creating an instance, verify from Symphony API: + + ```bash + curl -H "Content-Type: application/json" http://localhost:8082/v1alpha2/instances + curl -H "Content-Type: application/json" http://localhost:8082/v1alpha2/solutions + ``` + +And on the robot / Muto side check whether the stack launched: + + ```bash + ros2 topic list + ros2 topic echo /chatter + ``` + +## Creating a new stack archive (packaging a directory) + +If you want to create a `stack/archive` manifest from a directory containing a launch-based ROS stack, use `create_archive.sh` located in this folder. + +Usage + +1. Prepare a directory with the stack contents (for example the included `sample-stack/` directory which contains a `launch/` file). + +2. Run the script from the repo or this folder. The script expects two arguments: `` and ``. + +Example (create manifest in the same folder): + + ```bash + cd docs/samples/talker-listener + ./create_archive.sh sample-stack . + ``` + +This will produce a JSON file named like `-archive.json` containing: +- `metadata` (name/description/content_type) +- `launch.data`: base64-encoded tar.gz of the `sample-stack/` directory +- `launch.properties`: checksum, the launch file, flatten flag and other metadata + +You can then publish the generated file to Symphony with `define-solution.sh`: + + ```bash + cd ../symphony + ./define-solution.sh ../talker-listener/sample-stack-archive.json + ``` + +## Files in this folder + +- `talker-listener-json.json` — stack definition in plain JSON (content_type: `stack/json`) +- `talker-listener-json-instance.json` — instance file for the JSON stack +- `talker-listener-xarchive.json` — stack archive manifest (base64-encoded tar data) +- `talker-listener-xarchive-instance.json` — instance file for the archive stack +- `sample-stack-archive.json` — example pre-created archive manifest (created by `create_archive.sh`) +- `sample-stack/` — example directory that can be packaged with `create_archive.sh` +- `run-demo.sh` — automated end-to-end demo script (requires Symphony and Muto running) +- `create_archive.sh` — helper script that packages a directory into a `stack/archive` JSON manifest + +## Troubleshooting & tips + +- Authentication: the helper scripts assume Symphony API is running locally and accept the default admin login (empty password). Adjust `SYMPHONY_API_URL` and credentials in the scripts if your environment differs. +- Tools: `jq` and `curl` are required by the helper scripts. Install them with your OS package manager. +- If the Muto nodes do not appear after instance creation, check `ros2 node list`, the Muto logs, and ensure the Symphony provider in Muto is enabled (`enable_symphony:=true` when launching the Muto system). + +## Summary + +This folder demonstrates both the minimal JSON stack format and the archive-based stack format. Use `define-solution.sh` to publish stacks to Symphony and `define-instance.sh` to bind them to targets (robots). Use `create_archive.sh` to create new archive-type manifests from a directory containing a ROS launch stack. + +If you want me to also add example commands to automatically register a target (target JSON/templates are in `docs/samples/symphony/target.json`) or to create a small troubleshooting checklist, I can add that next. \ No newline at end of file diff --git a/docs/samples/talker-listener/create_archive.sh b/docs/samples/talker-listener/create_archive.sh new file mode 100755 index 0000000..59475d2 --- /dev/null +++ b/docs/samples/talker-listener/create_archive.sh @@ -0,0 +1,97 @@ +#!/bin/bash + +# Script to create a base64 encoded tar archive of a directory's contents and save it as JSON +# Usage: ./create_archive.sh + +# Input validation +if [ $# -ne 2 ]; then + echo "Usage: $0 " + echo " : Directory to archive" + echo " : Directory where the JSON file will be written" + exit 1 +fi + +INPUT_DIR="$1" +OUTPUT_DIR="$2" + +# Validate input directory +if [ ! -d "$INPUT_DIR" ]; then + echo "Error: $INPUT_DIR is not a directory" + exit 1 +fi + +# Validate output directory +if [ ! -d "$OUTPUT_DIR" ]; then + echo "Error: $OUTPUT_DIR is not a directory" + exit 1 +fi + + + +# Extract solution name from filename (remove path and .json extension) +ROOT_NAME=$(basename "$INPUT_DIR") +MANIFEST_NAME="${ROOT_NAME}-archive" + +# Function to encode base64 from stdin +encode_base64() { + # Read from stdin and encode to base64 + if [[ "$OSTYPE" == "darwin"* ]]; then + # macOS (BSD base64) + base64 + elif [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]] || [[ -n "$WSL_DISTRO_NAME" ]] || [[ -n "$IS_WSL" ]] || [[ "$(uname -r)" == *Microsoft* ]] || [[ "$(uname -r)" == *microsoft* ]]; then + # WSL/Windows environments + base64 | tr -d '\n' + else + # Linux/Unix (GNU base64) + base64 -w 0 + fi +} + +# Create tar archive of all contents and encode in base64 +STACK_DATA_BASE64=$(tar -C $INPUT_DIR -czf - . | encode_base64) + +# SHA256 checksum of the base64 encoded data +CHECKSUM=$(echo -n "$STACK_DATA_BASE64" | sha256sum | awk '{print $1}') +echo "SHA256 Checksum: $CHECKSUM" + +# Find the relative path of the first file that ends with launch.py +LAUNCH_FILE=$(cd "$INPUT_DIR" && find . -type f -name "*.launch.py" | head -n 1 | sed 's|^\./||') +if [ -z "$LAUNCH_FILE" ]; then + echo "Error: No launch.py file found in the directory" + exit 1 +fi +echo "Found launch file: $LAUNCH_FILE" + +# Create JSON manifest +STACK_MANIFEST=$(cat << EOF +{ + "metadata": { + "name": "${MANIFEST_NAME}", + "description": "A simple talker-listener stack example using demo_nodes_cpp package.", + "content_type": "stack/archive" + }, + "launch": { + "data": "${STACK_DATA_BASE64}", + "properties": { + "algorithm": "sha256", + "checksum": "${CHECKSUM}", + "launch_file": "${LAUNCH_FILE}", + "command": "launch", + "launch_args": [ + { + "name": "foo", + "default": "bar" + } + ], + "ros_args": [], + "flatten": true + } + } +} +EOF +) + +# Write to JSON file in output directory +JSON_FILE="$OUTPUT_DIR/${MANIFEST_NAME}.json" +echo "$STACK_MANIFEST" > "$JSON_FILE" +echo "Created stack manifest JSON file: $JSON_FILE" \ No newline at end of file diff --git a/docs/samples/talker-listener/run-demo.sh b/docs/samples/talker-listener/run-demo.sh new file mode 100755 index 0000000..85fd2c5 --- /dev/null +++ b/docs/samples/talker-listener/run-demo.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +# Lightweight end-to-end demo wrapper for the talker-listener sample. +# Assumes Symphony is already running; waits for the API, verifies Muto nodes, +# then defines the solution and creates an instance. +# Usage: ./run-demo.sh + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SYMPHONY_DIR="$SCRIPT_DIR/../symphony" +EXAMPLE="talker-listener-xarchive" +SOLUTION_JSON="$SCRIPT_DIR/${EXAMPLE}.json" +INSTANCE_JSON="$SCRIPT_DIR/${EXAMPLE}-instance.json" + +# Check requirements (we assume Symphony is already running) +for cmd in curl jq base64 ros2; do + if ! command -v "$cmd" >/dev/null 2>&1; then + echo "Required command not found: $cmd" >&2 + echo "Please install it and re-run the script." >&2 + exit 1 + fi +done + +echo "Assuming Symphony services are already running." + +# Wait for Symphony API to be available +API_URL="http://localhost:8082/v1alpha2/greetings" +echo "Waiting for Symphony API at $API_URL..." +for i in $(seq 1 60); do + if curl -sS "$API_URL" >/dev/null 2>&1; then + echo "Symphony API is up" + break + fi + printf "." + sleep 1 + if [ "$i" -eq 60 ]; then + printf '\nTimed out waiting for Symphony API\n' >&2 + exit 2 + fi +done + +# Note: solution/instance creation will be performed after verifying Muto nodes +echo "Symphony API is available. Will define solution & create instance after Muto nodes are verified." + +# Wait for expected Muto nodes to appear +REQUIRED_NODES=( + "/muto/agent" + "/muto/commands_plugin" + "/muto/compose_plugin" + "/muto/core_twin" + "/muto/gateway" + "/muto/launch_plugin" + "/muto/muto_composer" + "/muto/muto_symphony_provider" + "/muto/provision_plugin" +) + +echo "Waiting for Muto nodes to appear (timeout 60s)..." +missing=() +for i in $(seq 1 60); do + NODE_LIST=$(ros2 node list 2>/dev/null || true) + missing=() + for n in "${REQUIRED_NODES[@]}"; do + if ! printf '%s\n' "$NODE_LIST" | grep -F -x "$n" >/dev/null 2>&1; then + missing+=("$n") + fi + done + if [ ${#missing[@]} -eq 0 ]; then + echo "All Muto nodes are present" + break + fi + printf "." + sleep 1 + if [ "$i" -eq 60 ]; then + printf '\nTimed out waiting for Muto nodes. Missing: %s\n' "${missing[*]}" >&2 + exit 3 + fi +done + +# Define solution and create instance now that Muto nodes are present +echo "Defining solution using $SOLUTION_JSON" +"$SYMPHONY_DIR/define-solution.sh" "$SOLUTION_JSON" + +# Give Symphony a moment to register solution +sleep 1 + +echo "Creating instance using $INSTANCE_JSON" +"$SYMPHONY_DIR/define-instance.sh" "$INSTANCE_JSON" + +# Final status +echo "" +echo "---" +echo "Demo finished. Check Symphony instances and Muto:" +echo " http://localhost:3000 Symphony Portal" +echo " http://localhost:8082/v1alpha2/instances Symphony API" +echo "On the robot Muto verify nodes/topics:" +echo " ros2 node list | grep muto" +echo " ros2 topic echo /chatter" + +echo "Next steps:" +echo "1. To stop symphony, run:" +echo "cd $SYMPHONY_DIR && docker compose down" +echo "2. To delete the instance, run:" +echo "$SYMPHONY_DIR/delete-instance.sh $INSTANCE_JSON" +echo "3. To stop muto" +echo "ros2 daemon stop" + diff --git a/docs/samples/talker-listener/sample-stack/README.md b/docs/samples/talker-listener/sample-stack/README.md new file mode 100644 index 0000000..a8645cb --- /dev/null +++ b/docs/samples/talker-listener/sample-stack/README.md @@ -0,0 +1,60 @@ +# talker-listener-ws + +A ROS 2 Python workspace that pairs a talker and listener node on the `chatter` topic. The talker publishes a greeting once per second, and the listener logs every received message. Use this project as a minimal template for experimenting with ROS 2 publishers, subscribers, and launch files. + +## Repository Layout +- `config/talker.yaml` — parameter file loaded by the launch script (currently defines `publish_frequency` and `message`). +- `launch/talker_listener.launch.py` — starts both nodes in a single process using the configuration above. +- `src/muto_talker/` — Python package that exposes the `muto_talker` node. +- `src/muto_listener/` — Python package that exposes the `muto_listener` node. + +## Prerequisites +- A ROS 2 distribution (Foxy, Galactic, Humble, or Rolling) with Python support. +- `colcon` and `rosdep` installed for building and resolving dependencies. +- A sourced ROS 2 environment (`source /opt/ros//setup.bash`). + +## Build +```bash +# From the workspace root +rosdep install --from-paths src --ignore-src -r -y +colcon build +source install/setup.bash +``` +You can scope the build to one package at a time (e.g. `--packages-select muto_talker`) while iterating. + +## Run +After sourcing `install/setup.bash`: + +```bash +# Run each node separately +ros2 run muto_talker muto_talker +ros2 run muto_listener muto_listener + +# Launch both nodes together +ros2 launch --launch-file-path launch/talker_listener.launch.py +``` +> Tip: If you prefer `ros2 launch muto_talker talker_listener.launch.py`, add the launch file to the `data_files` section of the package setup so it is installed under `share/muto_talker/launch`. + +The talker publishes `std_msgs/msg/String` messages such as `"Hello SDV Hackathon Chapter III! 3"`; the listener prints each payload to its logger. + +## Configuration +The launch file loads `config/talker.yaml`, which currently defines: +```yaml +/talker: + ros__parameters: + publish_frequency: 1.0 + message: "Hello, SDV Hackathon Chapter III!" +``` +The node implementation still uses inline defaults (1 Hz and the string shown above). Update `muto_talker/muto_talker.py` to declare and read these parameters if you want runtime configurability. + +## Testing & Linting +The packages include the standard ROS 2 Python linters. +```bash +colcon test --packages-select muto_talker muto_listener +``` +This hooks `ament_flake8`, `ament_pep257`, and copyright checks. + +## Next Steps +- Extend the talker to declare parameters and use `publish_frequency` and `message` at runtime. +- Add more message types or QoS policies to experiment with ROS 2 communication strength. +- Package and install launch files so they are discoverable via `ros2 launch` without an explicit path. diff --git a/docs/samples/talker-listener/sample-stack/config/talker.yaml b/docs/samples/talker-listener/sample-stack/config/talker.yaml new file mode 100644 index 0000000..adec5af --- /dev/null +++ b/docs/samples/talker-listener/sample-stack/config/talker.yaml @@ -0,0 +1,4 @@ +/talker: + ros__parameters: + publish_frequency: 1.0 + message: "SDV Hackathon Chapter III!" \ No newline at end of file diff --git a/docs/samples/talker-listener/sample-stack/launch/talker_listener.launch.py b/docs/samples/talker-listener/sample-stack/launch/talker_listener.launch.py new file mode 100644 index 0000000..2808bc4 --- /dev/null +++ b/docs/samples/talker-listener/sample-stack/launch/talker_listener.launch.py @@ -0,0 +1,33 @@ +from launch import LaunchDescription +from launch_ros.actions import Node +import os + + +def generate_launch_description(): + talker_config = os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "..", + "config", + "talker.yaml", + ) + ) + node_talker = Node( + name="talker", + package="muto_talker", + executable="muto_talker", + output="screen", + parameters=[talker_config], + ) + + node_listener = Node( + name="listener", + package="muto_listener", + executable="muto_listener", + output="screen", + ) + + ld = LaunchDescription() + ld.add_action(node_talker) + ld.add_action(node_listener) + return ld diff --git a/docs/samples/talker-listener/sample-stack/src/muto_listener/muto_listener/__init__.py b/docs/samples/talker-listener/sample-stack/src/muto_listener/muto_listener/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/docs/samples/talker-listener/sample-stack/src/muto_listener/muto_listener/muto_listener.py b/docs/samples/talker-listener/sample-stack/src/muto_listener/muto_listener/muto_listener.py new file mode 100644 index 0000000..6003139 --- /dev/null +++ b/docs/samples/talker-listener/sample-stack/src/muto_listener/muto_listener/muto_listener.py @@ -0,0 +1,23 @@ +import rclpy +from rclpy.node import Node +from std_msgs.msg import String + +class MutoListener(Node): + def __init__(self): + super().__init__('muto_listener') + self.get_logger().info('MutoListener node has been started.') + self.create_subscription(String, 'chatter', self.listener_callback, 10) + + def listener_callback(self, msg): + self.get_logger().info(f'I heard: {msg.data}') + + +def main(args=None): + rclpy.init(args=args) + muto_listener = MutoListener() + rclpy.spin(muto_listener) + muto_listener.destroy_node() + rclpy.shutdown() + +if __name__ == '__main__': + main() diff --git a/docs/samples/talker-listener/sample-stack/src/muto_listener/package.xml b/docs/samples/talker-listener/sample-stack/src/muto_listener/package.xml new file mode 100644 index 0000000..5f94ab3 --- /dev/null +++ b/docs/samples/talker-listener/sample-stack/src/muto_listener/package.xml @@ -0,0 +1,18 @@ + + + + muto_listener + 0.0.0 + TODO: Package description + sel + TODO: License declaration + + ament_copyright + ament_flake8 + ament_pep257 + python3-pytest + + + ament_python + + diff --git a/docs/samples/talker-listener/sample-stack/src/muto_listener/resource/muto_listener b/docs/samples/talker-listener/sample-stack/src/muto_listener/resource/muto_listener new file mode 100644 index 0000000..e69de29 diff --git a/docs/samples/talker-listener/sample-stack/src/muto_listener/setup.cfg b/docs/samples/talker-listener/sample-stack/src/muto_listener/setup.cfg new file mode 100644 index 0000000..66d9342 --- /dev/null +++ b/docs/samples/talker-listener/sample-stack/src/muto_listener/setup.cfg @@ -0,0 +1,4 @@ +[develop] +script_dir=$base/lib/muto_listener +[install] +install_scripts=$base/lib/muto_listener diff --git a/docs/samples/talker-listener/sample-stack/src/muto_listener/setup.py b/docs/samples/talker-listener/sample-stack/src/muto_listener/setup.py new file mode 100644 index 0000000..ea8ed68 --- /dev/null +++ b/docs/samples/talker-listener/sample-stack/src/muto_listener/setup.py @@ -0,0 +1,26 @@ +from setuptools import find_packages, setup + +package_name = 'muto_listener' + +setup( + name=package_name, + version='0.0.0', + packages=find_packages(exclude=['test']), + data_files=[ + ('share/ament_index/resource_index/packages', + ['resource/' + package_name]), + ('share/' + package_name, ['package.xml']), + ], + install_requires=['setuptools'], + zip_safe=True, + maintainer='sel', + maintainer_email='ibrahim.sel@eteration.com', + description='TODO: Package description', + license='TODO: License declaration', + tests_require=['pytest'], + entry_points={ + 'console_scripts': [ + 'muto_listener = muto_listener.muto_listener:main' + ], + }, +) diff --git a/docs/samples/talker-listener/sample-stack/src/muto_talker/muto_talker/__init__.py b/docs/samples/talker-listener/sample-stack/src/muto_talker/muto_talker/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/docs/samples/talker-listener/sample-stack/src/muto_talker/muto_talker/muto_talker.py b/docs/samples/talker-listener/sample-stack/src/muto_talker/muto_talker/muto_talker.py new file mode 100644 index 0000000..dfecd57 --- /dev/null +++ b/docs/samples/talker-listener/sample-stack/src/muto_talker/muto_talker/muto_talker.py @@ -0,0 +1,31 @@ +import rclpy +from rclpy.node import Node +from std_msgs.msg import String + +class MutoTalker(Node): + def __init__(self): + super().__init__('muto_talker') + self.get_logger().info('MutoTalker node has been started.') + self.declare_parameter('publish_frequency', 1.0) + self.declare_parameter('message', 'Hello, SDV Hackathon Chapter III!') + + self.publisher_ = self.create_publisher(String, 'chatter', 10) + self.timer = self.create_timer(self.get_parameter('publish_frequency').get_parameter_value().double_value, self.timer_callback) + self.ct = 0 + + def timer_callback(self): + msg = String() + msg.data = self.get_parameter('message').get_parameter_value().string_value + f' {self.ct}' + self.publisher_.publish(msg) + self.ct += 1 + self.get_logger().info(f'Published: {msg.data}') + +def main(args=None): + rclpy.init(args=args) + muto_talker = MutoTalker() + rclpy.spin(muto_talker) + muto_talker.destroy_node() + rclpy.shutdown() + +if __name__ == '__main__': + main() diff --git a/docs/samples/talker-listener/sample-stack/src/muto_talker/package.xml b/docs/samples/talker-listener/sample-stack/src/muto_talker/package.xml new file mode 100644 index 0000000..abfbd89 --- /dev/null +++ b/docs/samples/talker-listener/sample-stack/src/muto_talker/package.xml @@ -0,0 +1,18 @@ + + + + muto_talker + 0.0.0 + TODO: Package description + sel + TODO: License declaration + + ament_copyright + ament_flake8 + ament_pep257 + python3-pytest + + + ament_python + + diff --git a/docs/samples/talker-listener/sample-stack/src/muto_talker/resource/muto_talker b/docs/samples/talker-listener/sample-stack/src/muto_talker/resource/muto_talker new file mode 100644 index 0000000..e69de29 diff --git a/docs/samples/talker-listener/sample-stack/src/muto_talker/setup.cfg b/docs/samples/talker-listener/sample-stack/src/muto_talker/setup.cfg new file mode 100644 index 0000000..6fc00b8 --- /dev/null +++ b/docs/samples/talker-listener/sample-stack/src/muto_talker/setup.cfg @@ -0,0 +1,4 @@ +[develop] +script_dir=$base/lib/muto_talker +[install] +install_scripts=$base/lib/muto_talker diff --git a/docs/samples/talker-listener/sample-stack/src/muto_talker/setup.py b/docs/samples/talker-listener/sample-stack/src/muto_talker/setup.py new file mode 100644 index 0000000..82d7b64 --- /dev/null +++ b/docs/samples/talker-listener/sample-stack/src/muto_talker/setup.py @@ -0,0 +1,26 @@ +from setuptools import find_packages, setup + +package_name = 'muto_talker' + +setup( + name=package_name, + version='0.0.0', + packages=find_packages(exclude=['test']), + data_files=[ + ('share/ament_index/resource_index/packages', + ['resource/' + package_name]), + ('share/' + package_name, ['package.xml']), + ], + install_requires=['setuptools'], + zip_safe=True, + maintainer='sel', + maintainer_email='ibrahim.sel@eteration.com', + description='TODO: Package description', + license='TODO: License declaration', + tests_require=['pytest'], + entry_points={ + 'console_scripts': [ + 'muto_talker = muto_talker.muto_talker:main' + ], + }, +) diff --git a/docs/samples/talker-listener/talker-listener-json-instance.json b/docs/samples/talker-listener/talker-listener-json-instance.json new file mode 100644 index 0000000..90dda37 --- /dev/null +++ b/docs/samples/talker-listener/talker-listener-json-instance.json @@ -0,0 +1,14 @@ +{ + "metadata": { + "name": "talker-listener-json-instance", + "labels": { + "muto": "demo" + } + }, + "spec": { + "solution": "talker-listener-json:1", + "target": { + "name": "test-robot-debug" + } + } +} \ No newline at end of file diff --git a/docs/samples/talker-listener/talker-listener-json.json b/docs/samples/talker-listener/talker-listener-json.json new file mode 100644 index 0000000..12bad96 --- /dev/null +++ b/docs/samples/talker-listener/talker-listener-json.json @@ -0,0 +1,21 @@ +{ + "metadata": { + "name": "Muto Simple Talker-Listener Stack", + "description": "A simple talker-listener stack example using demo_nodes_cpp package.", + "content_type": "stack/json" + }, + "launch": { + "node": [ + { + "name": "talker", + "pkg": "demo_nodes_cpp", + "exec": "talker" + }, + { + "name": "listener", + "pkg": "demo_nodes_cpp", + "exec": "listener" + } + ] + } +} \ No newline at end of file diff --git a/docs/samples/talker-listener/talker-listener-xarchive-instance.json b/docs/samples/talker-listener/talker-listener-xarchive-instance.json new file mode 100644 index 0000000..e8cee21 --- /dev/null +++ b/docs/samples/talker-listener/talker-listener-xarchive-instance.json @@ -0,0 +1,14 @@ +{ + "metadata": { + "name": "talker-listener-xarchive-instance", + "labels": { + "muto": "demo" + } + }, + "spec": { + "solution": "talker-listener-xarchive:1", + "target": { + "name": "test-robot-debug" + } + } +} \ No newline at end of file diff --git a/docs/samples/talker-listener/talker-listener-xarchive.json b/docs/samples/talker-listener/talker-listener-xarchive.json new file mode 100644 index 0000000..2669168 --- /dev/null +++ b/docs/samples/talker-listener/talker-listener-xarchive.json @@ -0,0 +1,25 @@ +{ + "metadata": { + "name": "Muto Simple Talker-Listener Stack", + "description": "A simple talker-listener stack example using demo_nodes_cpp package.", + "content_type": "stack/archive" + }, + "launch": { + "data": "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", + "properties": { + "algorithm": "sha256", + "checksum": "553fd2dc7d0eb41e7d65c467d358e7962d3efbb0e2f2e4f8158e926a081f96d0", + "launch_file": "launch/talker_listener.launch.py", + "command": "launch", + "launch_args": [ + { + "name": "arg1", + "default": "val1" + } + ], + "ros_args": [], + "flatten": true + } + } + +} diff --git a/docs/samples/talker-listener/test-robot-debug-target.json b/docs/samples/talker-listener/test-robot-debug-target.json new file mode 100644 index 0000000..f13d538 --- /dev/null +++ b/docs/samples/talker-listener/test-robot-debug-target.json @@ -0,0 +1,34 @@ +{ + "metadata": { + "name": "ankaios-target" + }, + "spec": { + "forceRedeploy": true, + "components": [ + { + "name": "muto", + "type": "muto-agent", + "properties": { + } + } + ], + "topologies": [ + { + "bindings": [ + { + "role": "muto", + "provider": "providers.target.mqtt", + "config": { + "name": "proxy", + "brokerAddress": "tcp://mosquitto:1883", + "clientID": "symphony", + "requestTopic": "coa-request", + "responseTopic": "coa-response", + "timeoutSeconds": "30" + } + } + ] + } + ] + } +} \ No newline at end of file diff --git a/launch/muto.launch.py b/launch/muto.launch.py index 3486033..2e9bf57 100644 --- a/launch/muto.launch.py +++ b/launch/muto.launch.py @@ -142,11 +142,11 @@ def generate_launch_description(): arguments=['--ros-args', '--log-level', log_level] ) - node_native_plugin = Node( + node_provision_plugin = Node( namespace=muto_namespace, - name="native_plugin", + name="provision_plugin", package="composer", - executable="native_plugin", + executable="provision_plugin", output="screen", parameters=[ muto_config_file, @@ -200,7 +200,7 @@ def generate_launch_description(): ld.add_action(node_twin) ld.add_action(node_composer) ld.add_action(node_compose_plugin) - ld.add_action(node_native_plugin) + ld.add_action(node_provision_plugin) ld.add_action(node_launch_plugin) ld.add_action(symphony_provider)