Architecture
RosFit is split into two halves — a lightweight bridge that runs on each robot, and a set of cloud services that ingest telemetry, manage state, and expose everything through a REST API and real-time dashboard. MQTT is the transport layer that connects the two.
Overview
The system is designed around a clear separation between the robot side and the cloud side, connected by an MQTT broker.
Robot Side — Each robot runs a RosFit Bridge process (Python, built on rclpy and paho-mqtt). The bridge subscribes to local ROS 2 topics, serializes messages to JSON, and publishes them to the MQTT broker. It also subscribes to command topics on the broker and injects them back into the ROS 2 graph.
Cloud Side — An EMQX 5.x broker receives all device traffic. Behind the broker, a Message Handler service routes messages to the appropriate backend: telemetry goes to TimescaleDB, device shadow updates go to Redis, and firmware artefacts are stored in MinIO. A FastAPI backend exposes a unified REST + WebSocket API that the React dashboard (and your own integrations) consume.
System diagram
┌─────────────────────────────────────────────────────────────────────────────┐
│ ROBOT SIDE │
│ │
│ ┌───────────┐ ┌───────────────┐ ┌──────────────┐ │
│ │ ROS 2 │───▶│ RosFit │───▶│ MQTT Client │──┐ │
│ │ Nodes │◀───│ Bridge │◀───│ (paho-mqtt) │ │ │
│ │ │ │ (rclpy) │ │ │ │ │
│ └───────────┘ └───────────────┘ └──────────────┘ │ │
│ │ │
└────────────────────────────────────────────────────────────┼────────────────┘
│
MQTT (TLS 8883 / WS 8084) │
│
┌────────────────────────────────────────────────────────────┼────────────────┐
│ CLOUD SIDE │ │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ EMQX 5.x Broker │ │
│ │ (MQTT + WebSocket) │ │
│ └──────────┬───────────┘ │
│ │ │
│ ┌─────────────────┼─────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Message │ │ Shadow │ │ OTA │ │
│ │ Handler │ │ Service │ │ Service │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ TimescaleDB │ │ Redis 7 │ │ MinIO │ │
│ │ (Postgres) │ │ (Cache) │ │ (Objects) │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ └─────────────────┼──────────────────┘ │
│ ▼ │
│ ┌──────────────┐ │
│ │ FastAPI │ │
│ │ Backend │ │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ React │ │
│ │ Dashboard │ │
│ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Data flow
Telemetry upstream (device to dashboard)
Data flows from the robot to the dashboard through a well-defined pipeline:
- A ROS 2 node publishes a message (e.g.
/odom,/battery_state). - The RosFit Bridge serializes the message to JSON and publishes it to
rosfit/{device_id}/telemetry/status. - EMQX receives the message and forwards it to the Message Handler via a shared subscription.
- The Message Handler validates the envelope, inserts the payload into TimescaleDB, and updates the device's last-seen timestamp in Redis.
- The FastAPI backend serves the data via REST (
GET /api/v1/telemetry/{device_id}) or pushes it to connected WebSocket clients. - The React dashboard renders the telemetry in real-time charts and fleet overview panels.
Commands downstream (dashboard to device)
- An operator clicks "Send Command" in the dashboard (or calls
POST /api/v1/commands/{device_id}). - The FastAPI backend wraps the command in a message envelope with a unique
correlation_idand publishes it torosfit/{device_id}/cmd/request. - EMQX delivers the message to the device's bridge.
- The bridge deserializes the command and publishes the corresponding ROS 2 message (e.g. a
Twiston/cmd_vel). - The bridge publishes an acknowledgement to
rosfit/{device_id}/cmd/responsewith the samecorrelation_id. - The Message Handler records the result, and the dashboard updates the command status.
Docker Compose stack
The entire platform runs as a set of Docker containers orchestrated by Docker Compose. A single docker compose up -d brings up every service.
| Service | Image | Ports | Purpose |
|---|---|---|---|
| emqx | emqx/emqx:5.x | 1883, 8883, 8083, 8084, 18083 | MQTT broker with TLS and WebSocket support |
| timescaledb | timescale/timescaledb:latest-pg15 | 5432 | Time-series telemetry storage (PostgreSQL extension) |
| redis | redis:7-alpine | 6379 | Device shadows, caching, pub/sub for real-time events |
| minio | minio/minio:latest | 9000, 9001 | S3-compatible object storage for firmware artefacts |
| api | rosfit/api:latest | 8000 | FastAPI backend (REST + WebSocket) |
| handler | rosfit/handler:latest | — | MQTT message consumer and router |
| dashboard | rosfit/dashboard:latest | 3000 | React + Vite frontend |
All services share a Docker network and communicate over internal DNS. External access is through the published ports listed above.
Technology stack
| Layer | Technology | Version | Role |
|---|---|---|---|
| MQTT Broker | EMQX | 5.x | High-performance broker with built-in auth, ACLs, and WebSocket |
| API | FastAPI (Python) | 0.110+ | REST and WebSocket API, JWT auth, background tasks |
| Database | TimescaleDB (PostgreSQL) | 15 | Hypertable-backed time-series storage for telemetry |
| Cache | Redis | 7.x | Device shadow state, session cache, real-time pub/sub |
| Object Storage | MinIO | latest | S3-compatible storage for OTA firmware and blob data |
| Frontend | React + Vite + Tailwind CSS | React 18, Vite 5 | Real-time fleet dashboard with charts and command panel |
| Bridge | Python (rclpy + paho-mqtt) | ROS 2 Humble / Iron | Bidirectional ROS 2 ↔ MQTT message bridge |
| Auth | JWT + X.509 | — | Device and user authentication with role-based access |