This file provides guidance to AI coding agents when working with code in this repository.
MiniQ-bot (Proyecto ENIGMA) is a quadruped robot developed by the Ceimtun RAS group at Universidad Nacional de Colombia. The goal is to build a functional quadruped prototype capable of at least two movement routines on different terrain types.
This project will be presented to international authorities such as IEEE and will serve as the foundation for a scientific paper following strict academic and research standards. All code, documentation, comments, and log messages must be written in English.
Key specs:
- 4 legs, 3 DOF each (12 Dynamixel AX-12A servos total)
- Minimum 2kg load capacity (own weight), each leg must support >0.5kg
- Impact-resistant 3D-printed structure
- Movement reaction time under 3 seconds
- LIDAR perception planned for autonomous navigation
- Target platform: ROS 2 on Ubuntu, C++ for all control nodes
# Source ROS 2 (required in every terminal)
source /opt/ros/humble/setup.bash
# Build (from repo root)
colcon build
# Build single package
colcon build --packages-select dynamixel_control_cpp
# Source workspace (required after every build, in every terminal)
source install/setup.bash
# Run the system (each in a separate terminal)
ros2 run dynamixel_control_cpp dynamixel_node # hardware driver
ros2 run dynamixel_control_cpp monitor_node # terminal dashboard
ros2 run dynamixel_control_cpp demo_node # motion demoOnly dynamixel_node communicates with physical servos. All other nodes interact exclusively through ROS 2 topics. This isolation is a hard rule: never add direct hardware access to non-driver nodes.
demo_node ──► /joint_commands ──► dynamixel_node ──► /joint_states ──► monitor_node
│
/dev/ttyUSB0
AX-12A servos (Protocol 1.0, 1Mbps)
Topics:
/joint_states(sensor_msgs/JointState) published at 20 Hz: position (degrees), velocity (raw), effort (load %)/joint_commands(sensor_msgs/JointState): goal positions in degrees
Servo naming convention: servo_<id> (e.g., servo_9). ID is parsed from the name via rfind('_').
- Dynamixel AX-12A servos, Protocol 1.0
- Position range: 0-1023 raw units (0-300 degrees)
- Serial:
/dev/ttyUSB0at 1,000,000 baud (configurable via ROS parameters) - AX-12 register constants are in the
ax12namespace indynamixel_node.cpp
- Create
.cppinsrc/dynamixel_control_cpp/src/ - Add
add_executable()andament_target_dependencies()inCMakeLists.txt - Add target to
install(TARGETS ...)block - If using a new ROS 2 dependency: add
<depend>inpackage.xmlandfind_package()inCMakeLists.txt(these must match)
This workflow is mandatory for all changes. Never commit directly to main. Every change, no matter how small, must go through the full cycle:
- Create a feature branch from
main:git checkout -b <type>/<short-description> main - Make commits on the branch using Conventional Commits (see below)
- Push the branch to remote:
git push -u origin <branch-name> - Open a Pull Request on GitHub
- Review the PR before merging (check the diff, verify nothing unintended is included)
- Merge the PR after review passes
- Delete the feature branch after merging (both remote and local)
Skipping any of these steps is not allowed. The review step (step 5) is not optional even when working solo.
Branch naming convention uses the same Conventional Commits types as prefix:
feat/<short-description> e.g. feat/sync-read
fix/<short-description> e.g. fix/temperature-threshold
docs/<short-description> e.g. docs/urdf-tutorial
refactor/<short-description> e.g. refactor/extract-servo-class
test/<short-description> e.g. test/kinematics-unit-tests
build/<short-description> e.g. build/add-nav-msgs-dep
chore/<short-description> e.g. chore/update-gitignore
All commits must follow the Conventional Commits specification. Write commit messages in clear, easy-to-understand language that provides context about why the change was made, not just what changed.
Format: <type>(<scope>): <description>
Types:
feat: a new feature or capabilityfix: a bug fixdocs: documentation changes onlyrefactor: code restructuring without changing behaviortest: adding or updating testsbuild: changes to build system or dependencies (CMakeLists.txt, package.xml)chore: maintenance tasks (gitignore, CI config)
Examples:
feat(dynamixel): add SyncRead for atomic multi-servo state readsfix(monitor_node): validate JointState array sizes before accessdocs: add ROS 2 beginner tutorial for new team membersbuild(dynamixel_control_cpp): add nav_msgs dependency for odometry
The description should explain the purpose, not repeat the diff. Prefer add temperature threshold parameter over change line 134 in dynamixel_node.cpp.
The development follows this sequence:
- Research quadruped kinematics (videos, URDF examples)
- Single-leg simulation (Matlab/Python or skip to URDF)
- Create/adapt URDF model for MiniQ-bot
- Select initial gait pattern (start with walk/"paso")
- 3D print and test single leg prototype
- Assemble leg, adjust for friction/weight/motor clearance
- Test leg strength, speed, and load capacity
- Partial robot assembly, verify interference with Dynamixel Wizard
- Integrate ROS 2 control with movement patterns and kinematic models
- Implement 3-support walking gait on flat terrain, then evaluate other gaits
All code in this repository must follow these rules. Based on guidelines from Henki Robotics.
- Build ROS 2 Nodes to have a single responsibility.
- Separate application logic into its own class or library, keeping ROS 2 nodes focused solely on communication. This improves code clarity, testability, and maintainability.
- Generate launch files in
launchfolder. - Prefer XML launch format. Python launch files are not intended to be used as the launch front-end, but can be still used if needed for flexibility.
- Avoid passing or hardcoding node parameters in launch files; use config files instead.
- Store ROS node parameters in YAML files under the
configfolder. - Treat package-level YAML files as "default" parameters. Users should not modify these files directly. Instead, they can override parameters in their own copies.
- If parameters are expected to be changed in runtime, make the parameters dynamic by defining parameter callbacks.
- Use ROS 2 logger instead of
print()andstd::coutstatements. - Use relevant log levels to avoid log noise:
INFO: System operates normally, logged for informational purposes.WARN: Unexpected, but recoverable condition. Might require action.ERROR: Critical failure. System no longer operates correctly. Requires immediate action.
- Avoid log spam. A usual mistake is to add logs on high-frequency callbacks, which flood the logs. For these cases, prefer throttled logs.
- Aim to reuse existing message interfaces from common_interfaces: geometry_msgs, sensor_msgs, nav_msgs, tf2_msgs.
- If there isn't a viable existing message type, create a custom message interface.
- Custom message interfaces should be in their own packages with
_msgssuffix. This allows easy message reuse without depending on the full package. - Avoid using primitive type messages from std_msgs or example_interfaces (Float32, Bool, String) as they are meant only for quick prototyping. Use a custom message with semantic meaning instead.
- This doesn't apply to general messages from std_msgs, such as Header.
- Enums in message interfaces can be simulated with constants.
- Use services only for very fast executing tasks (less than a second), such as requesting or setting a state. Use actions for tasks that take time to execute, can have multiple error cases, or may require cancellation.
- Prefer enum-style error codes instead of string messages when an action can fail for multiple reasons. This allows clients to reliably parse errors and enables user-friendly messages.
- Refer to the official ROS 2 code style guide.
- Follow the existing project coding style for all legacy code to maintain consistency. Apply updated best practices only to new nodes or when refactoring code.
- Prefer C++ over Python for performance-critical nodes with high-frequency control loops and high-bandwidth data. Python works well for tooling, high-level orchestration, testing, and prototyping.
- Use C++ composable nodes with intra-process communication enabled to pass large data such as images and point clouds between nodes. This avoids large memory and DDS overhead.
- Prefer
SingleThreadedExecutoroverMultiThreadedExecutorwhere possible. Single threaded applications are usually cleaner, easier to test, have deterministic execution order, and have a lower performance impact.- It can be tempting to use MultiThreadedExecutor for any node to allow synchronous communication inside other callbacks. This can often be avoided with good Node design or asynchronous communication.
- For applications that truly require multi-threading, MultiThreadedExecutor is a great option.
- Always define ROS 2 package dependencies on package-level.
- Use
package.xmlto automatically install dependencies usingrosdep. - If a package isn't available in rosdep, prefer to make a PR for rosdistro.
- Rosdep doesn't support exact version tagging. If you require a specific version of a Python package, use
requirements.txt.
- Use
- If the project requires a specific version of external dependencies to be built from source, use
vcstoolwith.reposfiles.- Use an exact version of the repository (commit hash or tag) to avoid regressions.
- Do not rebuild external ROS 2 packages just to change configuration or launch files. Instead, make a copy of the launch and config files in your workspace and modify them there.
- Document each ROS package with a
README.mdthat includes:- Short description and overview
- Usage
- API: Topics, Services, and Actions
- Parameters with type, description, and default value
- At minimum, write thorough unit tests for core application logic, using mocks where appropriate to isolate the tested functionality. If the application logic is well-separated from the ROS 2 nodes, the nodes themselves do not require unit tests.
- Test ROS 2 nodes, including their communication behavior, as part of integration testing.
- Aim for high test coverage (90-100%).
- Avoid using arbitrary sleeps in tests, as they can make tests non-deterministic and flaky. Instead of sleeping to wait for a ROS message, use synchronization mechanisms or wait for the expected result with a proper timeout.