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_serverpackage
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