diff --git a/.clang-format b/.clang-format
index bc4a490..63c3748 100644
--- a/.clang-format
+++ b/.clang-format
@@ -5,7 +5,10 @@ Standard: Auto
IndentWidth: 4
TabWidth: 4
UseTab: Never
-ColumnLimit: 110
+ColumnLimit: 150
+
+AccessModifierOffset: -4
+IndentAccessModifiers: true
AlignEscapedNewlinesLeft: false
AlignTrailingComments: true
diff --git a/.gitignore b/.gitignore
index cb98e60..5c45fc3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -427,3 +427,8 @@ build/
# pixi environments
.pixi/*
!.pixi/config.toml
+
+MUJOCO_LOG.TXT
+path_constants.yaml
+dockerfiles/booster_robotics_sdk
+dockerfiles/booster_robotics_sdk_ros2
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 2d0bd4d..ad62c72 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -10,6 +10,9 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}")
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
+set(QT_QML_GENERATE_QMLLS_INI ON) # For QML language server support
+file(WRITE "${CMAKE_CURRENT_SOURCE_DIR}/.clangd"
+ "CompileFlags:\n CompilationDatabase: ${CMAKE_CURRENT_BINARY_DIR}\n")
add_compile_definitions(PROJECT_ROOT="${CMAKE_SOURCE_DIR}")
include(FetchContent)
@@ -28,7 +31,7 @@ option(BUILD_PYTHON_BINDINGS "Build python bindings" ON)
# ============================= Dependencies ==================================================
find_package(Eigen3 REQUIRED)
find_package(OpenGL REQUIRED)
-find_package(Qt6 REQUIRED COMPONENTS Core Widgets OpenGLWidgets)
+find_package(Qt6 REQUIRED COMPONENTS Core Widgets OpenGLWidgets Quick Charts)
# Optional deps (linked only if found)
find_package(nlohmann_json QUIET)
@@ -36,8 +39,12 @@ find_package(CURL QUIET)
# Generate .clangd configuration after dependencies are found
get_target_property(EIGEN_INCLUDE_DIR Eigen3::Eigen INTERFACE_INCLUDE_DIRECTORIES)
+get_target_property(QT_CORE_INCLUDE_DIRS Qt6::Core INTERFACE_INCLUDE_DIRECTORIES)
+# Extract the base Qt6 include path (get parent of QtCore path)
+list(GET QT_CORE_INCLUDE_DIRS 0 QT_CORE_PATH)
+get_filename_component(QT_INCLUDE_BASE "${QT_CORE_PATH}" DIRECTORY)
file(WRITE "${CMAKE_CURRENT_SOURCE_DIR}/.clangd"
- "CompileFlags:\n CompilationDatabase: ${CMAKE_CURRENT_BINARY_DIR}\n Add:\n - -I${CMAKE_CURRENT_SOURCE_DIR}/include\n - -isystem${EIGEN_INCLUDE_DIR}\n")
+ "CompileFlags:\n CompilationDatabase: ${CMAKE_CURRENT_BINARY_DIR}\n Add:\n - -I${CMAKE_CURRENT_SOURCE_DIR}/include\n - -isystem${EIGEN_INCLUDE_DIR}\n - -isystem${QT_INCLUDE_BASE}\n - -isystem${QT_INCLUDE_BASE}/QtCore\n - -isystem${QT_INCLUDE_BASE}/QtWidgets\n - -isystem${QT_INCLUDE_BASE}/QtGui\n - -fPIC\n")
# -------------------- yaml-cpp (config -> module -> manual) [robust]
set(_YAML_HINT "/opt/homebrew/opt/yaml-cpp")
@@ -166,14 +173,20 @@ set(_EXTRA_LIBS
Qt6::Core
Qt6::Widgets
Qt6::OpenGLWidgets
- OpenGL::GL
+ Qt6::Quick
+ Qt6::Charts
${_PUGI_TGT}
CURL::libcurl
nlohmann_json::nlohmann_json
)
+# Add util library for PTY support on Linux
+if(UNIX AND NOT APPLE)
+ list(APPEND _EXTRA_LIBS util)
+endif()
+
# --- Build core library (ensure AUTOMOC is on for the target)
-add_library(${LIBRARY} STATIC ${SOURCES})
+add_library(${LIBRARY} STATIC ${SOURCES} ${QML_RESOURCES})
target_sources(${LIBRARY} PRIVATE ${HEADERS} ${ALL_UIS} ${ALL_QRCS}) # Just used for IDE discovering headers
set_target_properties(${LIBRARY} PROPERTIES AUTOMOC ON AUTOUIC ON AUTORCC ON)
diff --git a/dockerfiles/Dockerfile b/dockerfiles/Dockerfile
index 5683734..b57a800 100644
--- a/dockerfiles/Dockerfile
+++ b/dockerfiles/Dockerfile
@@ -1,9 +1,64 @@
-FROM ros:jazzy-ros-core
-
+FROM ros:humble-ros-base
ENV DEBIAN_FRONTEND=noninteractive
+# install dependencies
RUN apt-get update && apt-get install -y \
- libtinyxml2-dev && \
- rm -rf /var/lib/apt/lists/*
+ curl \
+ supervisor \
+ gnupg2 \
+ lsb-release \
+ build-essential \
+ libtinyxml2-dev \
+ libtinyxml2-9 \
+ libpulse0 \
+ libpulse-mainloop-glib0 \
+ libasound2 \
+ libxcursor1 \
+ libxinerama1 \
+ libxi6 \
+ libxfixes3 \
+ libxrandr2 \
+ libxss1 \
+ libxxf86vm1 \
+ libdrm2 \
+ libgbm1 \
+ libwayland-egl1 \
+ libwayland-client0 \
+ libwayland-cursor0 \
+ libxkbcommon0 \
+ libdecor-0-0 \
+ ament-cmake \
+ && rm -rf /var/lib/apt/lists/*
+
+# copy configs
+RUN mkdir /opt/booster
+RUN mkdir /opt/booster/configs
+COPY configs/system_settings_config.yaml /opt/booster/configs
+COPY booster.conf /etc/supervisor/conf.d/
+
+# copy and install booster sdk
+COPY booster_robotics_sdk /opt/booster/sdk
+RUN /opt/booster/sdk/install.sh
+
+# compile examples too (loco client in particular)
+RUN mkdir opt/booster/sdk/build
+RUN cd opt/booster/sdk/build && cmake .. && make
+
+# copy, compile and install booster ros sdk
+COPY booster_robotics_sdk_ros2 /opt/booster/sdk_ros2
+
+# Build with minimal optimization and sequential execution
+RUN /bin/bash -c "source /opt/ros/humble/setup.bash; cd /opt/booster/sdk_ros2 && colcon build"
+
+COPY LocoApiPackage /opt/booster/LocoApiPackage
+# RUN /bin/bash -c "source /opt/ros/humble/setup.bash; cd /opt/booster/LocoApiPackage && colcon build --packages-select booster_msgs
+
+# add custom bashrc
+COPY configs/bashrc /root/.bashrc_addendum
+RUN echo ". ~/.bashrc_addendum" >> ~/.bashrc
+
+RUN mkdir /app
+COPY entrypoint.sh /app
+RUN chmod +x /app/entrypoint.sh
-CMD ["bash", "-c", "/app/fake_framework/fake_framework & /app/BridgeSubscriber/BridgeSubscriber"]
+CMD ["/app/entrypoint.sh"]
diff --git a/dockerfiles/LocoApiPackage/booster_msgs/CMakeLists.txt b/dockerfiles/LocoApiPackage/booster_msgs/CMakeLists.txt
new file mode 100644
index 0000000..81dc731
--- /dev/null
+++ b/dockerfiles/LocoApiPackage/booster_msgs/CMakeLists.txt
@@ -0,0 +1,31 @@
+cmake_minimum_required(VERSION 3.8)
+project(booster_msgs)
+
+if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
+ add_compile_options(-Wall -Wextra -Wpedantic)
+endif()
+
+# find dependencies
+find_package(ament_cmake REQUIRED)
+find_package(rosidl_default_generators REQUIRED)
+find_package(std_msgs REQUIRED)
+
+# Generate interfaces
+rosidl_generate_interfaces(${PROJECT_NAME}
+ "msg/RpcReqMsg.msg"
+ DEPENDENCIES std_msgs
+)
+
+if(BUILD_TESTING)
+ find_package(ament_lint_auto REQUIRED)
+ # the following line skips the linter which checks for copyrights
+ # comment the line when a copyright and license is added to all source files
+ set(ament_cmake_copyright_FOUND TRUE)
+ # the following line skips cpplint (only works in a git repo)
+ # comment the line when this package is in a git repo and when
+ # a copyright and license is added to all source files
+ set(ament_cmake_cpplint_FOUND TRUE)
+ ament_lint_auto_find_test_dependencies()
+endif()
+
+ament_package()
diff --git a/dockerfiles/LocoApiPackage/booster_msgs/msg/RpcReqMsg.msg b/dockerfiles/LocoApiPackage/booster_msgs/msg/RpcReqMsg.msg
new file mode 100644
index 0000000..9d9ff86
--- /dev/null
+++ b/dockerfiles/LocoApiPackage/booster_msgs/msg/RpcReqMsg.msg
@@ -0,0 +1,3 @@
+string m_uuid
+string m_header
+string m_body
diff --git a/dockerfiles/LocoApiPackage/booster_msgs/package.xml b/dockerfiles/LocoApiPackage/booster_msgs/package.xml
new file mode 100644
index 0000000..97f493d
--- /dev/null
+++ b/dockerfiles/LocoApiPackage/booster_msgs/package.xml
@@ -0,0 +1,24 @@
+
+
+
+ booster_msgs
+ 0.0.0
+ TODO: Package description
+ flavio
+ TODO: License declaration
+
+ ament_cmake
+
+ rosidl_default_generators
+ rosidl_default_runtime
+ std_msgs
+
+ rosidl_interface_packages
+
+ ament_lint_auto
+ ament_lint_common
+
+
+ ament_cmake
+
+
diff --git a/dockerfiles/booster.conf b/dockerfiles/booster.conf
new file mode 100644
index 0000000..dfea72d
--- /dev/null
+++ b/dockerfiles/booster.conf
@@ -0,0 +1,18 @@
+[supervisord]
+nodaemon=true
+
+[program:booster-motion]
+directory=/app/booster_motion
+command=/app/booster_motion/booster-motion -mode sim -config ./configs/config_isaac.lua # DON'T CHANGE
+autostart=true
+autorestart=true
+stdout_logfile=/var/log/booster-motion.log
+stderr_logfile=/var/log/booster-motion.err
+
+[program:simbridge]
+directory=/app/bridge
+command=/app/bridge/simbridge
+autostart=true
+autorestart=true
+stdout_logfile=/var/log/simbridge.log
+stderr_logfile=/var/log/simbridge.err
diff --git a/dockerfiles/configs/bashrc b/dockerfiles/configs/bashrc
new file mode 100644
index 0000000..b401216
--- /dev/null
+++ b/dockerfiles/configs/bashrc
@@ -0,0 +1,16 @@
+source /opt/ros/humble/setup.sh
+source /opt/booster/sdk_ros2/install/setup.bash
+
+# Export dyanmic libraries and fastdds profile for booster_motion execution
+export LD_LIBRARY_PATH=/app/booster_motion/lib:/app/booster_motion/lib-usr-local:/app/booster_motion/lib-x86_64-linux-gnu:$LD_LIBRARY_PATH
+export FASTRTPS_DEFAULT_PROFILES_FILE=/app/booster_motion/fastdds_profile.xml
+
+alias loco="/opt/booster/sdk/build/b1_loco_example_client 127.0.0.1"
+alias looc="loco"
+alias lcoo="loco"
+alias mw="loco"
+alias mp="loco"
+
+alias echocmd="ros2 topic echo /booster/ros2_k2_joint_cmd"
+alias echostate="ros2 topic echo /booster/ros2_k2_joint_states"
+alias echolow="ros2 topic echo /low_state"
diff --git a/dockerfiles/configs/system_settings_config.yaml b/dockerfiles/configs/system_settings_config.yaml
new file mode 100644
index 0000000..5ab38bf
--- /dev/null
+++ b/dockerfiles/configs/system_settings_config.yaml
@@ -0,0 +1,4 @@
+audio_settings:
+ enable_dance_music: true
+debug_mode_settings:
+ enable_debug_mode: false
diff --git a/dockerfiles/entrypoint.sh b/dockerfiles/entrypoint.sh
new file mode 100644
index 0000000..a9cfb26
--- /dev/null
+++ b/dockerfiles/entrypoint.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+# Export dyanmic libraries and fastdds profile for booster_motion execution
+export LD_LIBRARY_PATH=/app/booster_motion/lib:/app/booster_motion/lib-usr-local:/app/booster_motion/lib-x86_64-linux-gnu:$LD_LIBRARY_PATH
+export FASTRTPS_DEFAULT_PROFILES_FILE=/app/booster_motion/fastdds_profile.xml
+
+
+# Start supervisord to manage processes
+/usr/bin/supervisord -n -c /etc/supervisor/conf.d/booster.conf
diff --git a/docs/circus/architecture.md b/docs/circus/architecture.md
new file mode 100644
index 0000000..b999808
--- /dev/null
+++ b/docs/circus/architecture.md
@@ -0,0 +1,378 @@
+# Architecture Overview
+
+Circus implements a distributed architecture where the physics simulation runs in the main process, while each robot's control software runs in a separate Docker container.
+
+We have adopted this design to ensure:
+
+- **Isolation**: Each robot runs in its own environment without interfering with others
+- **Flexibility**: Support for different robot types and frameworks during the same simulation run
+- **Low sim-to-real gap**: The same code that runs on real robots can run in simulation
+
+## Table of Contents
+
+- [System Architecture](#system-architecture)
+- [Docker Container Management](#docker-container-management)
+- [Communication Protocol](#communication-protocol)
+- [SimBridge Integration](#simbridge-integration)
+- [Complete Communication Flow](#complete-communication-flow)
+- [Supported Robots](#supported-robots)
+- [Adding New Robots](#adding-new-robots)
+
+
+## Docker Container Management
+
+The simulator manages Docker containers through two main components:
+
+### Container Class
+
+**Files:** [Container.h](), [Container.cpp]()
+
+The `Container` class handles the lifecycle of individual Docker containers using the Docker Engine API through Unix sockets.
+
+#### Container Creation
+
+When creating a container, the simulator configures:
+
+**Environment Variables:**
+
+- `ROBOT_NAME`: Unique identifier (format: `team_RobotType_number`)
+- `SERVER_IP`: Simulator address (typically `172.17.0.1` for Docker bridge)
+- `CIRCUS_PORT`: Communication port (default: 5555)
+
+**Docker Configuration:**
+
+- Volume bindings for shared data and code
+- Privileged mode for real-time performance
+- IPC mode set to `host` for shared memory
+- Security capabilities (`SYS_NICE`, `IPC_LOCK`) for real-time scheduling
+
+#### Docker API Communication
+
+The `Container` class uses cURL to communicate with the Docker daemon via the Unix socket `/var/run/docker.sock`:
+
+| Operation | HTTP Method | Endpoint | Purpose |
+|-----------|-------------|----------|---------|
+| Create | POST | `/containers/create?name={name}` | Create container instance |
+| Start | POST | `/containers/{id}/start` | Start the container |
+| Stop | POST | `/containers/{id}/stop?t=0` | Stop the container (SIGKILL after 0s) |
+| Remove | DELETE | `/containers/{id}` | Delete the container |
+
+
+### RobotManager Class
+
+**File:** [RobotManager.h]()
+
+The `RobotManager` is a singleton that orchestrates all robot containers and manages bidirectional communication between the simulator and robot frameworks.
+
+#### Responsibilities
+
+1. **Robot Registration**: Maintains a registry of all robots in the simulation
+2. **Container Lifecycle**: Creates, starts, and manages Docker containers
+3. **Network Server**: Runs a TCP server for robot-simulator communication
+4. **Message Routing**: Routes messages between simulator and correct robot containers
+5. **Synchronization**: Tracks when all robots are ready to begin simulation
+
+## Communication Protocol
+
+### Server Architecture
+
+The `RobotManager` implements a **polling-based TCP server** for efficient multi-client handling.
+
+#### Server Initialization
+
+1. Creates TCP socket and binds to specified port (default: 5555)
+2. Listens for incoming connections (up to number of robots)
+3. Uses `poll()` to monitor multiple connections simultaneously
+4. Runs in separate thread to avoid blocking simulation
+
+#### Connection Handshake
+
+When a Docker container starts, it initiates the connection:
+
+**Step 1: Container Connects**
+
+- Container creates TCP socket to `SERVER_IP:CIRCUS_PORT`
+- Simulator accepts connection and adds socket to polling list
+
+**Step 2: Robot Identification**
+
+- Container sends first message: robot name (MessagePack serialized string)
+- Simulator unpacks and validates robot name exists in registry
+- Robot marked as `isConnected = true`
+
+**Step 3: Initial State Exchange**
+
+- Simulator packs current robot state (joint positions, velocities, IMU data)
+- Sends initial state back to container
+- Container is now ready to begin control loop
+
+#### Message Exchange Loop
+
+The continuous communication follows a **request-response pattern**:
+
+**From Container to Simulator:**
+```json
+{
+ "robot_name": "red_Booster-T1_1",
+ "joint_torques": [0.0, 0.1, 0.2, "...", 0.0]
+}
+```
+
+**From Simulator to Container:**
+```json
+{
+ "robot_name": "red_Booster-T1_1",
+ "joint_positions": [0.0, 0.1, 0.2, "...", 0.0],
+ "joint_velocities": [0.0, 0.0, 0.0, "...", 0.0],
+ "imu_orientation": ["w", "x", "y", "z"],
+ "imu_angular_velocity": ["x", "y", "z"],
+ "imu_linear_acceleration": ["x", "y", "z"]
+}
+```
+
+#### Polling Mechanism
+
+The server uses `poll()` to efficiently handle multiple connections:
+
+```cpp
+poll(fds.data(), fds.size(), timeout_ms)
+```
+
+This allows:
+
+- Simultaneous monitoring of all robot connections
+- Non-blocking detection of new connections
+- Immediate response when data arrives
+- Timeout to check if server should continue running
+
+When data arrives on a socket:
+
+1. Read and unpack MessagePack message
+2. Extract `robot_name` to identify sender
+3. Find corresponding `Robot` object in registry
+4. Call `robot->receiveMessage()` to process commands
+5. Call `robot->sendMessage()` to get current state
+6. Pack and send response back to container
+
+## SimBridge Integration
+
+**Repository:** [SimBridge](https://github.com/SPQRTeam/simbridge.git)
+
+SimBridge is a ROS 2 node that runs inside each Docker container, acting as the translation layer between the robot control framework and the Circus simulator.
+
+### BridgeNode Architecture
+
+**Files:** [bridge_node.hpp](), [bridge_node.cpp](), [main.cpp]()
+
+#### Initialization Process
+
+**1. Environment Setup**
+
+The bridge reads three critical environment variables:
+
+```cpp
+ROBOT_NAME // e.g., "red_Booster-T1_1"
+SERVER_IP // e.g., "172.17.0.1"
+CIRCUS_PORT // e.g., "5555"
+```
+
+**2. Robot Type Detection**
+
+Extracts robot type from robot name using the format `team_TYPE_number`:
+
+```cpp
+// "red_Booster-T1_1" → "Booster-T1"
+size_t firstUnderscore = robot_name.find('_');
+size_t secondUnderscore = robot_name.find('_', firstUnderscore + 1);
+return robot_name.substr(firstUnderscore + 1, secondUnderscore - firstUnderscore - 1);
+```
+
+**3. Joint Name Mapping**
+
+Each robot type has a predefined joint order:
+
+**Booster-T1** (23 joints):
+```
+Head_yaw, Head_pitch,
+Left_Shoulder_Pitch, Left_Shoulder_Roll, Left_Elbow_Pitch, Left_Elbow_Yaw,
+Right_Shoulder_Pitch, Right_Shoulder_Roll, Right_Elbow_Pitch, Right_Elbow_Yaw,
+Waist,
+Left_Hip_Pitch, Left_Hip_Roll, Left_Hip_Yaw, Left_Knee_Pitch, Left_Ankle_Pitch, Left_Ankle_Roll,
+Right_Hip_Pitch, Right_Hip_Roll, Right_Hip_Yaw, Right_Knee_Pitch, Right_Ankle_Pitch, Right_Ankle_Roll
+```
+
+**Booster-K1** (22 joints):
+```
+Head_yaw, Head_pitch,
+Left_Shoulder_Pitch, Left_Shoulder_Roll, Left_Elbow_Pitch, Left_Elbow_Yaw,
+Right_Shoulder_Pitch, Right_Shoulder_Roll, Right_Elbow_Pitch, Right_Elbow_Yaw,
+Left_Hip_Pitch, Left_Hip_Roll, Left_Hip_Yaw, Left_Knee_Pitch, Left_Ankle_Pitch, Left_Ankle_Roll,
+Right_Hip_Pitch, Right_Hip_Roll, Right_Hip_Yaw, Right_Knee_Pitch, Right_Ankle_Pitch, Right_Ankle_Roll
+```
+
+**4. Socket Connection**
+
+Establishes TCP connection to simulator:
+```cpp
+socket(AF_INET, SOCK_STREAM, 0) // Create socket
+connect(server_addr) // Connect to SERVER_IP:CIRCUS_PORT
+send(robot_name) // Send identification message
+```
+
+#### ROS 2 Topics
+
+SimBridge creates publishers and subscribers for robot communication:
+
+**Subscriptions** (commands from framework):
+
+- `/joint_command` (`sensor_msgs::msg::JointState`) - Desired joint torques/efforts
+
+**Publications** (sensor data to framework):
+
+- `/joint_state` (`sensor_msgs::msg::JointState`) - Current joint positions and velocities
+- `/imu` (`sensor_msgs::msg::Imu`) - IMU orientation, angular velocity, linear acceleration
+
+#### Communication Flow
+
+**Sending Commands to Simulator**:
+
+1. Robot framework publishes desired joint commands to `/joint_command` topic
+2. `jointCommandCallback()` receives the ROS message
+3. Extracts joint efforts (torques) from the message
+4. Creates MessagePack map: `{robot_name, joint_torques}`
+5. Serializes and sends via TCP socket to simulator
+
+```cpp
+void jointCommandCallback(const sensor_msgs::msg::JointState::SharedPtr msg) {
+ std::map data_map;
+ data_map["robot_name"] = msgpack::object(robotName_);
+ data_map["joint_torques"] = msgpack::object(msg->effort);
+
+ msgpack::sbuffer sbuf;
+ msgpack::pack(sbuf, data_map);
+ send(client_fd, sbuf.data(), sbuf.size(), 0);
+}
+```
+
+**Receiving State from Simulator**:
+
+1. Periodic timer triggers `receiveAndPublish()` at fixed rate (typically 100Hz)
+2. `receiveMessageFromSimulator_()` reads from TCP socket
+3. Unpacks MessagePack message and validates robot name
+4. Extracts joint states and IMU data from message
+5. Creates ROS messages with current timestamps
+6. Publishes to `/joint_state` and `/imu` topics
+7. Robot framework receives sensor data and computes next control action
+
+#### Error Handling
+
+The bridge implements robust error handling ([bridge_node.cpp:147](../../simbridge/src/bridge_node.cpp#L147)):
+
+- **Timeout**: Returns gracefully if no data within timeout period
+- **Connection Closure**: Detects when simulator closes connection
+- **Parse Errors**: Catches MessagePack deserialization failures
+- **Robot Name Validation**: Ensures messages are for correct robot
+- **Socket Errors**: Handles network issues gracefully
+
+## Complete Communication Flow
+
+[TODO] Add image
+
+### Timing and Synchronization
+
+- **Physics timestep**: Typically 0.001s (1000Hz) in MuJoCo
+- **Communication rate**: Typically 0.01s (100Hz) between simulator and bridge
+- **Control rate**: Depends on framework, typically 100Hz or higher
+- **Synchronization**: Each robot must receive commands before simulation advances
+
+## Supported Robots
+
+Currently, Circus supports the following humanoid robots:
+
+### Booster-T1
+
+- **Joints**: 23 DOF
+- **Features**: Includes waist joint for torso rotation
+- **Joint configuration**: Full arm and leg articulation with head movement
+- **Use case**: Research platform for bipedal locomotion with upper body motion
+
+### Booster-K1
+
+- **Joints**: 22 DOF
+- **Features**: Fixed torso without waist joint
+- **Joint configuration**: Simplified version of T1
+- **Use case**: Locomotion-focused research without upper body complexity
+
+Both robots share the same communication protocol but differ in joint count and names. The SimBridge automatically selects the correct joint mapping based on the robot type extracted from the robot name.
+
+## Adding New Robots
+
+To integrate a new robot type into Circus:
+
+### 1. Create Robot Model
+
+Add the robot URDF/MJCF model to the simulator's model directory.
+
+### 2. Implement Robot Class
+
+Create a new C++ class inheriting from the `Robot` base class:
+
+```cpp
+// include/robots/MyRobot.h
+class MyRobot : public Robot {
+public:
+ MyRobot(const std::string& name, const std::string& type, ...);
+
+ void receiveMessage(const std::map& data) override;
+ std::map sendMessage() override;
+};
+```
+
+### 3. Register in RobotFactory
+
+Add your robot to the factory map in [RobotManager.h:324](../../include/RobotManager.h#L324):
+
+```cpp
+std::unordered_map robotFactory = {
+ {"Booster-K1", ...},
+ {"Booster-T1", ...},
+ {"MyRobot", [](auto&& name, auto&& type, ...) {
+ return std::make_shared(name, type, ...);
+ }}
+};
+```
+
+### 4. Define Joint Mapping in SimBridge
+
+Add joint names to [bridge_node.cpp:4](../../simbridge/src/bridge_node.cpp#L4):
+
+```cpp
+const std::map> BridgeNode::ROBOT_JOINTS_NAMES_MAP = {
+ {"Booster-T1", {...}},
+ {"Booster-K1", {...}},
+ {"MyRobot", {
+ "joint_1", "joint_2", "joint_3", "..."
+ }}
+};
+```
+
+**Important**: Joint order must match the order in your robot's URDF/MJCF model.
+
+### 5. Build Docker Image
+
+Create or update a Docker image containing:
+- SimBridge executable
+- Your robot's control framework
+- All required dependencies
+
+### 6. Update Configuration
+
+Modify configuration files to reference your new robot type and Docker image.
+
+### 7. Test Integration
+
+1. Start simulator with your new robot type
+2. Verify container starts successfully
+3. Check TCP connection establishes
+4. Confirm sensor data flows to framework
+5. Verify commands affect simulated robot
diff --git a/docs/circus/installation.md b/docs/circus/installation.md
new file mode 100644
index 0000000..fb013c7
--- /dev/null
+++ b/docs/circus/installation.md
@@ -0,0 +1,140 @@
+# Installation
+
+Circus is a MuJoCo-based simulator for humanoid robots. The project uses [pixi](https://pixi.prefix.dev/latest/) for dependency management.
+
+## Prerequisites
+
+Before installing Circus, ensure you have the following:
+
+- **Pixi**: Package manager for the project. Install from the [official website](https://pixi.prefix.dev/latest/)
+- **Docker**: Required for running robot control frameworks in isolated containers
+
+## Architecture Overview
+
+Circus uses a distributed architecture where each robot runs inside its own Docker container, communicating with the simulator through TCP sockets. We have implemented [SimBridge](https://github.com/SPQRTeam/simbridge.git), a ROS 2-based bridge that enables communication between Docker containers and the simulator. While SimBridge is designed for Booster T1/K1 robots, it can be easily adapted for other robot types or serve as a reference for implementing custom bridges.
+
+For a detailed explanation of the system architecture, see [Circus Architecture Overview](./architecture_overview.md).
+
+## Installation Steps
+
+### 1. Install Circus Simulator
+
+Clone and install the Circus simulator:
+
+```bash
+git clone https://github.com/SPQRTeam/circus.git
+cd circus
+pixi install
+```
+
+### 2. Build SimBridge
+
+SimBridge can be installed in any location on your machine:
+
+```bash
+git clone https://github.com/SPQRTeam/simbridge.git
+cd simbridge
+pixi install
+pixi run build
+```
+
+### 3. Setup Booster Robotics SDK
+
+Navigate to the Circus `dockerfiles` directory and clone the required SDK repositories:
+
+```bash
+cd /path/to/circus/dockerfiles
+git clone https://github.com/BoosterRobotics/booster_robotics_sdk.git
+git clone https://github.com/BoosterRobotics/booster_robotics_sdk_ros2.git
+```
+
+### 4. Fix ROS 2 Message Definition
+
+There is a known issue in the `booster_robotics_sdk_ros2` repository. You need to fix the `Subtitle.msg` file:
+
+**File:** `booster_robotics_sdk_ros2/booster_ros2_interface/msg/Subtitle.msg`
+
+Replace the entire content with:
+
+```
+string magic_number
+string text
+string language
+string user_id # Indicates the source of the subtitle. If it's from the robot, it will be the fixed value "BoosterRobot". Otherwise, it represents a human and may be a random string.
+int32 seq # subtitle sequence
+bool definite # Indicates whether the subtitle is a complete sentence.
+bool paragraph # Indicates whether the subtitle is a complete paragraph.
+int32 round_id
+```
+
+### 5. Build Docker Image
+
+Build the Docker image that will run the robot control framework:
+
+```bash
+cd /path/to/circus
+docker build -t spqr:booster dockerfiles/
+```
+
+This creates a Docker image named `spqr:booster` containing SimBridge and the Booster SDK.
+
+### 6. Configure Path Constants
+
+Create a configuration file to specify the locations of all required components:
+
+**File:** `circus/resources/config/path_constants.yaml`
+
+```yaml
+framework: /absolute/path/to/spqrbooster2026
+circus: /absolute/path/to/circus
+simbridge: /absolute/path/to/simbridge
+booster_robotics_sdk: /absolute/path/to/booster_robotics_sdk
+```
+
+**Important:** All paths must be absolute paths, not relative paths.
+
+## Running the Simulator
+
+Once all installation steps are complete, launch the simulator:
+
+```bash
+cd /path/to/circus
+pixi run circus
+```
+
+The simulator will:
+
+1. Start the MuJoCo physics engine
+2. Load the robot models and scene
+3. Create and start Docker containers for each robot
+4. Establish TCP connections between containers and the simulator
+5. Begin the simulation loop
+
+## Troubleshooting
+
+### Docker Connection Issues
+
+If robots fail to connect, verify:
+- Docker daemon is running: `docker ps`
+- Port 5555 is available: `netstat -an | grep 5555`
+- Docker image was built successfully: `docker images | grep spqr`
+
+### Path Configuration Issues
+
+If the simulator can't find required files:
+- Verify all paths in `path_constants.yaml` are absolute
+- Check that directories exist and are readable
+- Ensure the `framework` path points to your robot control code
+
+### SimBridge Build Issues
+
+If SimBridge fails to build:
+- Ensure all pixi dependencies are installed: `pixi install`
+- Check ROS 2 environment is properly configured
+- Verify the `Subtitle.msg` fix was applied correctly
+
+## Next Steps
+
+- Read the [Architecture Overview](./architecture_overview.md) to understand how Circus works
+- Explore the configuration files in `resources/config/` to customize your simulation
+- Check the examples to see how to create multi-robot scenarios
diff --git a/docs/getting_started/introduction.md b/docs/circus/introduction.md
similarity index 100%
rename from docs/getting_started/introduction.md
rename to docs/circus/introduction.md
diff --git a/docs/getting_started/installation.md b/docs/getting_started/installation.md
deleted file mode 100644
index 378964f..0000000
--- a/docs/getting_started/installation.md
+++ /dev/null
@@ -1,29 +0,0 @@
-# Installation
-
-## Linux
-
-Tested on Ubuntu 24.04
-
-1. **Install dependencies**
-```bash
-sudo apt update
-sudo apt install -y curl git
-```
-
-2. **Clone the repository**
-```bash
-git clone https://github.com/DaniAffCH/circus.git
-cd circus
-```
-
-3. **Run the installation script**
-```bash
-./install.sh
-```
-
-4. **Restart your terminal** to apply environment changes.
-
-5. **Run the application** using Pixi
-```bash
-pixi run circus-main
-```
diff --git a/docs/getting_started/quick_start_guide.md b/docs/getting_started/quick_start_guide.md
deleted file mode 100644
index f2fc45f..0000000
--- a/docs/getting_started/quick_start_guide.md
+++ /dev/null
@@ -1 +0,0 @@
-# Quick Start Guide
diff --git a/include/AppWindow.h b/include/AppWindow.h
index 590b804..e4fb68d 100644
--- a/include/AppWindow.h
+++ b/include/AppWindow.h
@@ -1,32 +1,55 @@
#pragma once
#include
+#include
+#include
+#include
+#include
+#include
#include
+#include
#include
+#include
+#include
#include
#include "MujocoContext.h"
#include "SimulationThread.h"
#include "SimulationViewport.h"
+#include "frontend/game_controller_panel_column/GameControllerPanelColumnContainer.h"
+#include "frontend/game_controller_panel_header/GameControllerPanelHeaderContainer.h"
+#include "frontend/tools_panel/ToolsPanel.h"
+
namespace spqr {
class AppWindow : public QMainWindow {
- public:
- AppWindow(int& argc, char** argv);
- ~AppWindow();
+ Q_OBJECT
+
+ public:
+ AppWindow(int& argc, char** argv);
+ ~AppWindow();
+
+ private slots:
+ void openScene();
+ void playSimulation();
+ void pauseSimulation();
- private:
- void loadScene(const QString& xml);
- void openScene();
+ private:
+ void loadScene(const QString& yaml_file);
+ static void signalHandler(int signal);
- static void signalHandler(int signal);
+ QVBoxLayout* mainLayout = nullptr;
+ QHBoxLayout* contentLayout = nullptr;
+ QWidget* viewportContainer = nullptr;
+ QLabel* viewportPlaceholder = nullptr;
- QVBoxLayout* mainLayout;
- QWidget* viewportContainer;
+ GameControllerPanelColumnContainer* gameControllerPanelColumnContainer = nullptr;
+ GameControllerPanelHeaderContainer* gameControllerPanelHeaderContainer = nullptr;
+ ToolsPanel* toolsPanel = nullptr;
- std::unique_ptr mujContext;
- std::unique_ptr viewport;
- std::unique_ptr sim;
+ std::unique_ptr mujContext;
+ std::unique_ptr viewport;
+ std::unique_ptr sim;
};
} // namespace spqr
diff --git a/include/CircusApplication.h b/include/CircusApplication.h
index ee3bde4..9e1a5a8 100644
--- a/include/CircusApplication.h
+++ b/include/CircusApplication.h
@@ -4,9 +4,9 @@
namespace spqr {
class CircusApplication : public QApplication {
- public:
- CircusApplication(int& argc, char** argv);
- ~CircusApplication();
+ public:
+ CircusApplication(int& argc, char** argv);
+ ~CircusApplication();
};
} // namespace spqr
diff --git a/include/Constants.h b/include/Constants.h
index 8d82b80..7eef67e 100644
--- a/include/Constants.h
+++ b/include/Constants.h
@@ -2,8 +2,9 @@
namespace spqr {
constexpr const char* appName = "Circus Simulator";
-constexpr unsigned initialWindowWidth = 800;
-constexpr unsigned initialWindowHeight = 600;
+constexpr unsigned initialWindowWidth = 1200;
+constexpr unsigned initialWindowHeight = 900;
constexpr const char* frameworkConfigPath = "resources/config/framework_config.yaml";
+constexpr const char* pathsConfigPath = "resources/config/path_constants.yaml";
constexpr int frameworkCommunicationPort = 5555;
} // namespace spqr
diff --git a/include/Container.h b/include/Container.h
index c8164a1..1a0860e 100644
--- a/include/Container.h
+++ b/include/Container.h
@@ -6,30 +6,33 @@
#include "curl/curl.h"
namespace spqr {
+
+enum class ContainerState { NONE, IDLE, RUNNING, REMOVED };
class Container {
- public:
- // TODO: is the path always correct for Unix systems??
- Container(const std::string& name, const std::string& sockPath = "/var/run/docker.sock");
- ~Container();
+ public:
+ // TODO: is the path always correct for Unix systems??
+ Container(const std::string& name, const std::string& sockPath = "/var/run/docker.sock");
+ ~Container();
- void create(const std::string& robot_name, const std::string& image,
- const std::vector& binds);
+ void create(const std::string& robot_name, const std::string& image, const std::vector& binds);
- void start();
- void stop();
- void remove();
+ void start();
+ void stop();
+ void remove();
- private:
- enum class ContainerState { NONE, IDLE, RUNNING, REMOVED };
+ std::string getId() const {
+ return id;
+ }
- std::string request(const std::string& method, const std::string& endpoint, const long expected_response,
- const nlohmann::json* body = nullptr);
+ private:
+ std::string request(const std::string& method, const std::string& endpoint, const long expected_response,
+ const nlohmann::json* body = nullptr);
- std::string id;
- ContainerState state;
- std::string name;
+ std::string id;
+ ContainerState state;
+ std::string name;
- std::string sockPath;
- CURL* curl_handle;
+ std::string sockPath;
+ CURL* curl_handle;
};
} // namespace spqr
diff --git a/include/FieldGenerator.h b/include/FieldGenerator.h
new file mode 100644
index 0000000..037f4df
--- /dev/null
+++ b/include/FieldGenerator.h
@@ -0,0 +1,97 @@
+#pragma once
+
+#include
+#include
+#include
+
+namespace spqr {
+
+struct FieldConfig;
+
+struct LineSegment {
+ float x1, y1, z1; // Start point
+ float x2, y2, z2; // End point
+};
+
+class FieldGenerator {
+ public:
+ /**
+ * Generate MuJoCo XML for a field based on field configuration
+ * @param fieldConfig Field dimensions and specifications
+ * @param meshDir Base directory (unused, kept for API compatibility)
+ * @return XML string containing the complete field definition
+ */
+ static std::string generateFieldXML(const FieldConfig& fieldConfig, const std::string& meshDir);
+
+ /**
+ * Generate field XML and append it to an existing mujoco node
+ * @param mujocoNode Parent mujoco XML node
+ * @param fieldConfig Field dimensions and specifications
+ * @param meshDir Base directory (unused, kept for API compatibility)
+ */
+ static void appendFieldToMuJoCo(pugi::xml_node& mujocoNode, const FieldConfig& fieldConfig, const std::string& meshDir);
+
+ private:
+ /**
+ * Add field assets (textures, materials, goal meshes) to the asset node
+ */
+ static void addFieldAssets(pugi::xml_node& assetNode, const FieldConfig& fieldConfig, const std::string& meshDir);
+
+ /**
+ * Add field geometries (ground, lines, goals) to the worldbody node
+ */
+ static void addFieldGeometries(pugi::xml_node& worldbodyNode, const FieldConfig& fieldConfig);
+
+ /**
+ * Add the ground plane geom
+ */
+ static void addGroundPlane(pugi::xml_node& worldbodyNode, const FieldConfig& fieldConfig);
+
+ /**
+ * Calculate all line segments that make up the field lines based on field configuration
+ * This includes: boundary lines, halfway line, center circle, goal areas, penalty areas
+ */
+ static std::vector calculateFieldLines(const FieldConfig& fieldConfig);
+
+ /**
+ * Add field lines as individual box/capsule geoms
+ * Lines are generated procedurally based on field dimensions
+ */
+ static void addFieldLines(pugi::xml_node& worldbodyNode, const FieldConfig& fieldConfig);
+
+ /**
+ * Add a single line segment as a box or capsule geom
+ * @param worldbodyNode Parent worldbody node
+ * @param name Geom name
+ * @param segment Line segment definition
+ * @param width Line width
+ */
+ static void addLineSegment(pugi::xml_node& worldbodyNode, const std::string& name, const LineSegment& segment, float width);
+
+ /**
+ * Add center circle as a composite geom (multiple segments forming a circle)
+ */
+ static void addCenterCircle(pugi::xml_node& worldbodyNode, const FieldConfig& fieldConfig);
+
+ /**
+ * Add goal structure at specified position and rotation
+ * @param worldbodyNode Parent worldbody node
+ * @param fieldConfig Field configuration with goal dimensions
+ * @param goalPrefix Name prefix for goal geoms (e.g., "left_goal", "right_goal")
+ * @param xPosition X position of the goal
+ * @param yawRotation Yaw rotation in radians (0 or π)
+ */
+ static void addGoal(pugi::xml_node& worldbodyNode, const FieldConfig& fieldConfig, const std::string& goalPrefix, float xPosition,
+ float yawRotation);
+
+ /**
+ * Add ball to the scene
+ * @param assetNode Parent asset node for ball materials
+ * @param worldbodyNode Parent worldbody node
+ * @param ballRadius Ball radius from field config
+ * @param initialPosition Initial ball position (x, y, z)
+ */
+ static void addBall(pugi::xml_node& assetNode, pugi::xml_node& worldbodyNode, float ballRadius, const std::array& initialPosition);
+};
+
+} // namespace spqr
diff --git a/include/GameController.h b/include/GameController.h
new file mode 100644
index 0000000..a73c525
--- /dev/null
+++ b/include/GameController.h
@@ -0,0 +1,326 @@
+#pragma once
+
+#include
+#include