Skip to main content

ROS/ROS 2 Implementation Guide

This guide explains how to deploy a ROS/ROS2 DeviceShifu-based robot device, covering setup, a sample ROS driver, deploying in Shifu and validation.


1. Architecture Diagram


2. Prerequisites

2.1 Robot

  • Ubuntu with ROS/ROS 2 installed
  • rosbridge_server package

2.2 Host (Linux)

  • Docker
  • k3s
  • kubectl

2.3 Shifu Installation

Before deploying any ROS device drivers, make sure the Shifu is installed on your edge cluster.

You can install Shifu using the following command :

kubectl apply -f https://raw.githubusercontent.com/Edgenesis/shifu/v0.70.0/pkg/k8s/crd/install/shifu_install.yml

Wait for all Shifu pods to be in the Running state:

kubectl get pods -n shifu-crd-system

3. ROS-side Setup

This example uses a ROS-based robot car.

3.1 Install ROS Bridge

sudo apt-get update
sudo apt-get install ros-noetic-rosbridge-server
# Note: If you are using a different ROS distribution, replace "noetic" in the commands with the corresponding version name.

3.2 Launch Required Robot Nodes (ROS/ROS 2)

ROS

roscore
source /opt/ros/noetic/setup.bash
roslaunch <your_robot_package> <your_drive_launch_file>.launch
roslaunch <your_robot_package> <your_camera_launch_file>.launch
roslaunch <your_robot_package> <your_lidar_launch_file>.launch

ROS 2

source /opt/ros/humble/setup.bash
ros2 launch <your_robot_package> <your_drive_launch_file>.launch
ros2 launch <your_robot_package> <your_camera_launch_file>.launch
ros2 launch <your_robot_package> <your_lidar_launch_file>.launch

3.3 Launch the rosbridge WebSocket

ROS

roslaunch rosbridge_server rosbridge_websocket.launch

ROS 2

ros2 launch rosbridge_server rosbridge_websocket.launch 

Note: Make sure each terminal stays open and reports that the node is running.


4. Write, Package, and Deploy a Custom ROS Driver to k3s

4.1 Sample ROS Driver

package main

import (
"context"
"encoding/json"
"log"
"net/http"
"os"
"time"

"github.com/coder/websocket"
"github.com/gin-gonic/gin"
)

type ROSMessage struct {
Op string `json:"op"`
Topic string `json:"topic"`
Type string `json:"type,omitempty"`
Msg interface{} `json:"msg,omitempty"`
}

type Twist struct {
Linear Vector3 `json:"linear"`
Angular Vector3 `json:"angular"`
}

type Vector3 struct {
X float64 `json:"x"`
Y float64 `json:"y"`
Z float64 `json:"z"`
}

var rosConn *websocket.Conn

func connectRosbridge() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

var err error
rosConn, _, err = websocket.Dial(ctx, "ws://"+os.Getenv("WEBSOCKET_BRIDGE_ADDRESS"), &websocket.DialOptions{
CompressionMode: websocket.CompressionDisabled,
})
if err != nil {
log.Fatalf("Failed to connect to rosbridge_server: %v", err)
}
rosConn.SetReadLimit(20 * 1 << 20)
log.Println("Connected to rosbridge_server")
}

func publishCmdVel(linear, angular float64) error {
cmd := ROSMessage{
Op: "publish",
Topic: "/cmd_vel",
Msg: Twist{
Linear: Vector3{X: linear, Y: 0.0, Z: 0.0},
Angular: Vector3{X: 0.0, Y: 0.0, Z: angular},
},
}
data, _ := json.Marshal(cmd)
return rosConn.Write(context.Background(), websocket.MessageText, data)
}

func main() {
connectRosbridge()

r := gin.Default()

// Only a single demo API: move forward
r.POST("/move/forward", func(c *gin.Context) {
if err := publishCmdVel(0.5, 0); err != nil {
c.String(http.StatusInternalServerError, "Failed to move forward")
} else {
c.String(http.StatusOK, "Moving forward")
}
})

// Listen on port specified by HTTP_PORT env var
r.Run(":" + os.Getenv("HTTP_PORT"))
}

4.2 Sample ROS 2 Driver

package main

import (
"context"
"log"
"net/http"
"os"

"github.com/gin-gonic/gin"
"github.com/tiiuae/rclgo/pkg/rclgo"
geometry_msgs "github.com/tiiuae/rclgo-msgs/geometry_msgs/msg"
)

func main() {
// Initialize the ROS 2 node
node, err := rclgo.NewNode("go_cmd_vel_driver", "")
if err != nil {
log.Fatalf("Failed to create ROS 2 node: %v", err)
}
defer node.Close()

// Create a publisher for the /cmd_vel topic
pub, err := node.NewPublisher("/cmd_vel", geometry_msgs.TwistTypeSupport)
if err != nil {
log.Fatalf("Failed to create publisher: %v", err)
}
defer pub.Close()

r := gin.Default()

// Forward movement endpoint
r.POST("/move/forward", func(c *gin.Context) {
msg := &geometry_msgs.Twist{}
msg.Linear.X = 0.5
msg.Angular.Z = 0.0
if err := pub.Publish(msg); err != nil {
c.String(http.StatusInternalServerError, "Failed to move forward")
} else {
c.String(http.StatusOK, "Moving forward")
}
})

// Start the ROS 2 executor in a separate goroutine
go func() {
rclgo.Spin(context.Background(), node)
}()

// Start HTTP server
port := os.Getenv("HTTP_PORT")
if port == "" {
port = "8080"
}
r.Run(":" + port)
}

4.3 Build and Push the Docker Image

Create a new Dockerfile:

# Build stage
FROM --platform=$BUILDPLATFORM golang:1.24.1-alpine as builder

WORKDIR /app

# Copy go.mod and go.sum
COPY go.mod go.sum ./

# Download dependencies
RUN go mod download

# Copy source code
COPY . .

# Build the Go app
ARG TARGETOS
ARG TARGETARCH
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -a -o /output/rosDeviceShifu main.go

# Final stage
# FROM gcr.io/distroless/static-debian11
FROM alpine:latest

WORKDIR /app

# Copy the binary from builder
COPY --from=builder /output/rosDeviceShifu .

# Expose ROS port
EXPOSE 9090

# Use non-root user for security
USER 65532:65532

# Run the application
ENTRYPOINT ["./rosDeviceShifu"]

Run the following commands

ROS/ROS 2

docker build -t <YOUR_REGSTRY>/ros-motion-driver:latest .
docker push <YOUR_REGISTRY>/ros-motion-driver:late

4.4 Create Configuration YAML

ROS/ROS 2

apiVersion: apps/v1
kind: Deployment
metadata:
name: deviceshifu-ros
namespace: deviceshifu
labels:
app: deviceshifu-ros
spec:
replicas: 1
selector:
matchLabels:
app: deviceshifu-ros
template:
metadata:
labels:
app: deviceshifu-ros
spec:
serviceAccountName: edgedevice-sa
volumes:
- name: deviceshifu-ros
configMap:
name: deviceshifu-ros
containers:
- image: edgehub/deviceshifu-http-http:v0.70.0
name: deviceshifu-http
ports:
- containerPort: 8080
volumeMounts:
- name: deviceshifu-ros
mountPath: "/etc/edgedevice/config"
readOnly: true
env:
- name: EDGEDEVICE_NAME
value: "deviceshifu-ros"
- name: EDGEDEVICE_NAMESPACE
value: "devices"
- name: ros1-driver
image: edgehub/ros-driver:latest #Replace with your own image name.
env:
- name: WEBSOCKET_BRIDGE_ADDRESS
value: 192.168.31.101:9090 #Enter the actual device IP
- name: HTTP_PORT
value: "5001"
ports:
- containerPort: 5001
name: http
---
apiVersion: v1
kind: Service
metadata:
name: deviceshifu-ros
namespace: deviceshifu
labels:
app: deviceshifu-ros
spec:
type: ClusterIP
selector:
app: deviceshifu-ros
ports:
- name: http
protocol: TCP
port: 80
targetPort: 8080
---
apiVersion: shifu.edgenesis.io/v1alpha1
kind: EdgeDevice
metadata:
name: deviceshifu-ros
namespace: devices
spec:
sku: deviceshifu-ros
protocol: HTTP
address: localhost:5001
---
apiVersion: v1
kind: ConfigMap
metadata:
name: deviceshifu-ros
namespace: deviceshifu
data:
driverProperties: |
driverSku:
driverImage:
instructions: |
instructions:
move/forward:
telemetries: |
telemetrySettings:

4.5 Deploy the deviceShifu

kubectl apply -f myrobot-shifu.yaml

5. Expose & Verify

5.1 deviceshifu-ros Service Port Forwarding

kubectl -n deviceshifu port-forward svc/deviceshifu-wheeltec-car-ros1-service 3000:80

5.2 Test Control and Data Endpoints

curl http://localhost:3000/move/forward