Add initial project structure with CMake configuration, source files, and README documentation for Unitree GO2 Custom Controller

This commit is contained in:
2025-09-07 16:43:22 +08:00
parent b2b6ddf245
commit 6acff483dd
16 changed files with 3429 additions and 1 deletions

316
src/config.cpp Normal file
View File

@@ -0,0 +1,316 @@
#include "config.hpp"
#include "logger.hpp"
#include <fstream>
#include <iostream>
namespace custom {
Config::Config() {
// Load default configuration
loadDefaults();
}
Config& Config::getInstance() {
static Config instance;
return instance;
}
void Config::loadDefaults() {
using namespace config;
config_.network_interface = NETWORK_INTERFACE;
config_.mqtt_broker = MQTT_BROKER;
config_.mqtt_port = MQTT_PORT;
config_.mqtt_username = MQTT_USERNAME;
config_.mqtt_password = MQTT_PASSWORD;
config_.mqtt_client_id = MQTT_CLIENT_ID;
config_.topic_prefix = TOPIC_PREFIX;
config_.cmd_topic = TOPIC_CMD;
config_.state_topic = TOPIC_STATE;
config_.video_topic = TOPIC_VIDEO;
config_.audio_topic = TOPIC_AUDIO;
config_.control_frequency = CONTROL_FREQUENCY;
config_.state_publish_frequency = STATE_PUBLISH_FREQUENCY;
config_.enable_video = ENABLE_VIDEO;
config_.enable_audio = ENABLE_AUDIO;
config_.max_linear_velocity = MAX_LINEAR_VELOCITY;
config_.max_angular_velocity = MAX_ANGULAR_VELOCITY;
config_.emergency_stop_timeout = EMERGENCY_STOP_TIMEOUT;
config_.stand_height = STAND_HEIGHT;
config_.default_gait = GAIT;
config_.video_width = VIDEO_WIDTH;
config_.video_height = VIDEO_HEIGHT;
config_.video_fps = VIDEO_FPS;
config_.video_format = VIDEO_FORMAT;
config_.audio_sample_rate = AUDIO_SAMPLE_RATE;
config_.audio_channels = AUDIO_CHANNELS;
config_.audio_format = AUDIO_FORMAT;
}
void Config::loadHighPerformancePreset() {
loadDefaults();
config_.control_frequency = HighPerformancePreset::CONTROL_FREQUENCY;
config_.state_publish_frequency = HighPerformancePreset::STATE_PUBLISH_FREQUENCY;
config_.max_linear_velocity = HighPerformancePreset::MAX_LINEAR_VELOCITY;
config_.max_angular_velocity = HighPerformancePreset::MAX_ANGULAR_VELOCITY;
config_.enable_video = HighPerformancePreset::ENABLE_VIDEO;
config_.enable_audio = HighPerformancePreset::ENABLE_AUDIO;
}
void Config::loadDevelopmentPreset() {
loadDefaults();
config_.control_frequency = DevelopmentPreset::CONTROL_FREQUENCY;
config_.state_publish_frequency = DevelopmentPreset::STATE_PUBLISH_FREQUENCY;
config_.max_linear_velocity = DevelopmentPreset::MAX_LINEAR_VELOCITY;
config_.max_angular_velocity = DevelopmentPreset::MAX_ANGULAR_VELOCITY;
config_.enable_video = DevelopmentPreset::ENABLE_VIDEO;
config_.enable_audio = DevelopmentPreset::ENABLE_AUDIO;
}
void Config::loadSafetyPreset() {
loadDefaults();
config_.control_frequency = SafetyPreset::CONTROL_FREQUENCY;
config_.state_publish_frequency = SafetyPreset::STATE_PUBLISH_FREQUENCY;
config_.max_linear_velocity = SafetyPreset::MAX_LINEAR_VELOCITY;
config_.max_angular_velocity = SafetyPreset::MAX_ANGULAR_VELOCITY;
config_.emergency_stop_timeout = SafetyPreset::EMERGENCY_STOP_TIMEOUT;
config_.enable_video = SafetyPreset::ENABLE_VIDEO;
config_.enable_audio = SafetyPreset::ENABLE_AUDIO;
}
bool Config::loadConfig(const std::string& configFile) {
// If compile-time config is enabled, skip JSON loading
if (config::USE_COMPILE_TIME_CONFIG) {
loadDefaults();
return true;
}
try {
std::ifstream file(configFile);
if (!file.is_open()) {
LOG_WARN("Config file not found: " + configFile + ", using defaults");
setDefaults();
return false;
}
nlohmann::json j;
file >> j;
// Load network settings
if (j.contains("network")) {
auto& network = j["network"];
if (network.contains("interface")) {
config_.network_interface = network["interface"];
}
}
// Load MQTT settings
if (j.contains("mqtt")) {
auto& mqtt = j["mqtt"];
if (mqtt.contains("broker")) config_.mqtt_broker = mqtt["broker"];
if (mqtt.contains("port")) config_.mqtt_port = mqtt["port"];
if (mqtt.contains("username")) config_.mqtt_username = mqtt["username"];
if (mqtt.contains("password")) config_.mqtt_password = mqtt["password"];
if (mqtt.contains("client_id")) config_.mqtt_client_id = mqtt["client_id"];
}
// Load topics
if (j.contains("topics")) {
auto& topics = j["topics"];
if (topics.contains("prefix")) config_.topic_prefix = topics["prefix"];
if (topics.contains("cmd")) config_.cmd_topic = topics["cmd"];
if (topics.contains("state")) config_.state_topic = topics["state"];
if (topics.contains("video")) config_.video_topic = topics["video"];
if (topics.contains("audio")) config_.audio_topic = topics["audio"];
}
// Load robot settings
if (j.contains("robot")) {
auto& robot = j["robot"];
if (robot.contains("control_frequency")) config_.control_frequency = robot["control_frequency"];
if (robot.contains("state_publish_frequency")) config_.state_publish_frequency = robot["state_publish_frequency"];
if (robot.contains("enable_video")) config_.enable_video = robot["enable_video"];
if (robot.contains("enable_audio")) config_.enable_audio = robot["enable_audio"];
}
// Load safety settings
if (j.contains("safety")) {
auto& safety = j["safety"];
if (safety.contains("max_linear_velocity")) config_.max_linear_velocity = safety["max_linear_velocity"];
if (safety.contains("max_angular_velocity")) config_.max_angular_velocity = safety["max_angular_velocity"];
if (safety.contains("emergency_stop_timeout")) config_.emergency_stop_timeout = safety["emergency_stop_timeout"];
}
// Load motion settings
if (j.contains("motion")) {
auto& motion = j["motion"];
if (motion.contains("stand_height")) config_.stand_height = motion["stand_height"];
if (motion.contains("default_gait")) config_.default_gait = motion["default_gait"];
}
// Load video settings
if (j.contains("video")) {
auto& video = j["video"];
if (video.contains("width")) config_.video_width = video["width"];
if (video.contains("height")) config_.video_height = video["height"];
if (video.contains("fps")) config_.video_fps = video["fps"];
if (video.contains("format")) config_.video_format = video["format"];
}
// Load audio settings
if (j.contains("audio")) {
auto& audio = j["audio"];
if (audio.contains("sample_rate")) config_.audio_sample_rate = audio["sample_rate"];
if (audio.contains("channels")) config_.audio_channels = audio["channels"];
if (audio.contains("format")) config_.audio_format = audio["format"];
}
LOG_INFO("Configuration loaded successfully from: " + configFile);
return true;
} catch (const std::exception& e) {
LOG_ERROR("Failed to load config: " + std::string(e.what()));
setDefaults();
return false;
}
}
bool Config::saveConfig(const std::string& configFile) {
try {
nlohmann::json j;
// Network settings
j["network"]["interface"] = config_.network_interface;
// MQTT settings
j["mqtt"]["broker"] = config_.mqtt_broker;
j["mqtt"]["port"] = config_.mqtt_port;
j["mqtt"]["username"] = config_.mqtt_username;
j["mqtt"]["password"] = config_.mqtt_password;
j["mqtt"]["client_id"] = config_.mqtt_client_id;
// Topics
j["topics"]["prefix"] = config_.topic_prefix;
j["topics"]["cmd"] = config_.cmd_topic;
j["topics"]["state"] = config_.state_topic;
j["topics"]["video"] = config_.video_topic;
j["topics"]["audio"] = config_.audio_topic;
// Robot settings
j["robot"]["control_frequency"] = config_.control_frequency;
j["robot"]["state_publish_frequency"] = config_.state_publish_frequency;
j["robot"]["enable_video"] = config_.enable_video;
j["robot"]["enable_audio"] = config_.enable_audio;
// Safety settings
j["safety"]["max_linear_velocity"] = config_.max_linear_velocity;
j["safety"]["max_angular_velocity"] = config_.max_angular_velocity;
j["safety"]["emergency_stop_timeout"] = config_.emergency_stop_timeout;
// Motion settings
j["motion"]["stand_height"] = config_.stand_height;
j["motion"]["default_gait"] = config_.default_gait;
// Video settings
j["video"]["width"] = config_.video_width;
j["video"]["height"] = config_.video_height;
j["video"]["fps"] = config_.video_fps;
j["video"]["format"] = config_.video_format;
// Audio settings
j["audio"]["sample_rate"] = config_.audio_sample_rate;
j["audio"]["channels"] = config_.audio_channels;
j["audio"]["format"] = config_.audio_format;
std::ofstream file(configFile);
if (!file.is_open()) {
LOG_ERROR("Failed to open config file for writing: " + configFile);
return false;
}
file << j.dump(4) << std::endl;
LOG_INFO("Configuration saved to: " + configFile);
return true;
} catch (const std::exception& e) {
LOG_ERROR("Failed to save config: " + std::string(e.what()));
return false;
}
}
std::string Config::getFullTopic(const std::string& topic) const {
return config_.topic_prefix + "/" + topic;
}
bool Config::validateConfig() {
bool valid = true;
// Validate frequencies
if (config_.control_frequency <= 0 || config_.control_frequency > 1000) {
LOG_WARN("Invalid control frequency, setting to default");
config_.control_frequency = config::CONTROL_FREQUENCY;
valid = false;
}
if (config_.state_publish_frequency <= 0 || config_.state_publish_frequency > 500) {
LOG_WARN("Invalid state publish frequency, setting to default");
config_.state_publish_frequency = config::STATE_PUBLISH_FREQUENCY;
valid = false;
}
// Validate velocities
if (config_.max_linear_velocity <= 0 || config_.max_linear_velocity > 5.0) {
LOG_WARN("Invalid max linear velocity, setting to default");
config_.max_linear_velocity = config::MAX_LINEAR_VELOCITY;
valid = false;
}
if (config_.max_angular_velocity <= 0 || config_.max_angular_velocity > 10.0) {
LOG_WARN("Invalid max angular velocity, setting to default");
config_.max_angular_velocity = config::MAX_ANGULAR_VELOCITY;
valid = false;
}
// Validate timeout
if (config_.emergency_stop_timeout <= 0 || config_.emergency_stop_timeout > 60.0) {
LOG_WARN("Invalid emergency stop timeout, setting to default");
config_.emergency_stop_timeout = config::EMERGENCY_STOP_TIMEOUT;
valid = false;
}
// Validate video settings
if (config_.video_width <= 0 || config_.video_height <= 0) {
LOG_WARN("Invalid video dimensions, setting to default");
config_.video_width = config::VIDEO_WIDTH;
config_.video_height = config::VIDEO_HEIGHT;
valid = false;
}
if (config_.video_fps <= 0 || config_.video_fps > 120) {
LOG_WARN("Invalid video FPS, setting to default");
config_.video_fps = config::VIDEO_FPS;
valid = false;
}
// Validate audio settings
if (config_.audio_sample_rate <= 0) {
LOG_WARN("Invalid audio sample rate, setting to default");
config_.audio_sample_rate = config::AUDIO_SAMPLE_RATE;
valid = false;
}
if (config_.audio_channels <= 0) {
LOG_WARN("Invalid audio channels, setting to default");
config_.audio_channels = config::AUDIO_CHANNELS;
valid = false;
}
return valid;
}
} // namespace custom

575
src/controller.cpp Normal file
View File

@@ -0,0 +1,575 @@
#include "controller.hpp"
#include "config.hpp"
#include "logger.hpp"
#include <unitree/robot/channel/channel_factory.hpp>
#include <unitree/common/thread/thread.hpp>
namespace custom {
Controller::Controller(const std::string& networkInterface)
: networkInterface_(networkInterface), running_(false), initialized_(false),
emergencyStopActive_(false) {
auto& config = Config::getInstance().getConfig();
controlPeriod_ = std::chrono::milliseconds(static_cast<int>(1000.0 / config.control_frequency));
statePeriod_ = std::chrono::milliseconds(static_cast<int>(1000.0 / config.state_publish_frequency));
}
Controller::~Controller() {
shutdown();
}
bool Controller::initialize() {
try {
LOG_INFO("Initializing robot controller with interface: " + networkInterface_);
// Initialize channel factory
unitree::robot::ChannelFactory::Instance()->Init(0, networkInterface_);
// Initialize sport client
sportClient_ = std::make_unique<unitree::robot::go2::SportClient>();
sportClient_->SetTimeout(10.0f);
sportClient_->Init();
// Initialize obstacle avoidance client
obstacleClient_ = std::make_unique<unitree::robot::go2::ObstaclesAvoidClient>();
obstacleClient_->Init();
// Initialize video client
videoClient_ = std::make_unique<unitree::robot::go2::VideoClient>();
videoClient_->Init();
// Initialize VUI client
vuiClient_ = std::make_unique<unitree::robot::go2::VuiClient>();
vuiClient_->Init();
// Initialize state subscribers
sportStateSubscriber_.reset(
new unitree::robot::ChannelSubscriber<unitree_go::msg::dds_::SportModeState_>("rt/sportmodestate")
);
sportStateSubscriber_->InitChannel(
std::bind(&Controller::updateSportModeState, this, std::placeholders::_1), 1
);
lowStateSubscriber_.reset(
new unitree::robot::ChannelSubscriber<unitree_go::msg::dds_::LowState_>("rt/lowstate")
);
lowStateSubscriber_->InitChannel(
std::bind(&Controller::updateLowLevelState, this, std::placeholders::_1), 1
);
// Initialize robot state
{
std::lock_guard<std::mutex> lock(stateMutex_);
currentState_ = RobotState{};
currentState_.timestamp = std::chrono::steady_clock::now();
}
initialized_ = true;
LOG_INFO("Robot controller initialized successfully");
return true;
} catch (const std::exception& e) {
LOG_ERROR("Failed to initialize robot controller: " + std::string(e.what()));
return false;
}
}
void Controller::shutdown() {
if (running_) {
stop();
}
LOG_INFO("Shutting down robot controller");
// Reset all clients
sportClient_.reset();
obstacleClient_.reset();
videoClient_.reset();
vuiClient_.reset();
// Reset subscribers
sportStateSubscriber_.reset();
lowStateSubscriber_.reset();
initialized_ = false;
}
bool Controller::start() {
if (!initialized_) {
LOG_ERROR("Cannot start: robot controller not initialized");
return false;
}
if (running_) {
LOG_WARN("Robot controller already running");
return true;
}
LOG_INFO("Starting robot controller");
running_ = true;
emergencyStopActive_ = false;
lastCommandTime_ = std::chrono::steady_clock::now();
// Start control threads
controlThread_ = std::thread(&Controller::controlLoop, this);
stateThread_ = std::thread(&Controller::stateUpdateLoop, this);
LOG_INFO("Robot controller started successfully");
return true;
}
bool Controller::stop() {
if (!running_) {
return true;
}
LOG_INFO("Stopping robot controller");
// Stop all motion first
emergencyStop();
running_ = false;
commandCondition_.notify_all();
// Wait for threads to finish
if (controlThread_.joinable()) {
controlThread_.join();
}
if (stateThread_.joinable()) {
stateThread_.join();
}
LOG_INFO("Robot controller stopped");
return true;
}
RobotState Controller::getCurrentState() const {
std::lock_guard<std::mutex> lock(stateMutex_);
return currentState_;
}
void Controller::setStateCallback(StateCallback callback) {
stateCallback_ = callback;
}
void Controller::setErrorCallback(ErrorCallback callback) {
errorCallback_ = callback;
}
bool Controller::executeCommand(const MotionCommand& command) {
if (!running_) {
LOG_ERROR("Cannot execute command: robot controller not running");
return false;
}
if (emergencyStopActive_) {
LOG_WARN("Cannot execute command: emergency stop active");
return false;
}
if (!validateCommand(command)) {
LOG_ERROR("Invalid motion command");
return false;
}
{
std::lock_guard<std::mutex> lock(commandMutex_);
commandQueue_.push(command);
lastCommandTime_ = std::chrono::steady_clock::now();
}
commandCondition_.notify_one();
return true;
}
bool Controller::emergencyStop() {
LOG_WARN("Emergency stop activated");
emergencyStopActive_ = true;
try {
if (sportClient_) {
sportClient_->StopMove();
sportClient_->Damp();
}
return true;
} catch (const std::exception& e) {
LOG_ERROR("Emergency stop failed: " + std::string(e.what()));
return false;
}
}
bool Controller::recoveryStand() {
LOG_INFO("Executing recovery stand");
try {
if (sportClient_) {
int32_t result = sportClient_->RecoveryStand();
if (result == 0) {
emergencyStopActive_ = false;
return true;
}
}
return false;
} catch (const std::exception& e) {
LOG_ERROR("Recovery stand failed: " + std::string(e.what()));
return false;
}
}
// Basic motions
bool Controller::standUp() {
LOG_INFO("Standing up");
try {
return sportClient_ && sportClient_->StandUp() == 0;
} catch (const std::exception& e) {
LOG_ERROR("Stand up failed: " + std::string(e.what()));
return false;
}
}
bool Controller::standDown() {
LOG_INFO("Standing down");
try {
return sportClient_ && sportClient_->StandDown() == 0;
} catch (const std::exception& e) {
LOG_ERROR("Stand down failed: " + std::string(e.what()));
return false;
}
}
bool Controller::sit() {
LOG_INFO("Sitting");
try {
return sportClient_ && sportClient_->Sit() == 0;
} catch (const std::exception& e) {
LOG_ERROR("Sit failed: " + std::string(e.what()));
return false;
}
}
bool Controller::lie() {
LOG_INFO("Lying down");
try {
return sportClient_ && sportClient_->StandDown() == 0;
} catch (const std::exception& e) {
LOG_ERROR("Lie down failed: " + std::string(e.what()));
return false;
}
}
bool Controller::damp() {
LOG_INFO("Damping");
try {
return sportClient_ && sportClient_->Damp() == 0;
} catch (const std::exception& e) {
LOG_ERROR("Damp failed: " + std::string(e.what()));
return false;
}
}
// Movement
bool Controller::move(double vx, double vy, double vyaw) {
try {
auto& config = Config::getInstance().getConfig();
// Apply safety limits
vx = std::max(-config.max_linear_velocity, std::min(config.max_linear_velocity, vx));
vy = std::max(-config.max_linear_velocity, std::min(config.max_linear_velocity, vy));
vyaw = std::max(-config.max_angular_velocity, std::min(config.max_angular_velocity, vyaw));
return sportClient_ && sportClient_->Move(vx, vy, vyaw) == 0;
} catch (const std::exception& e) {
LOG_ERROR("Move failed: " + std::string(e.what()));
return false;
}
}
bool Controller::moveToPosition(double x, double y, double yaw) {
try {
return obstacleClient_ && obstacleClient_->MoveToAbsolutePosition(x, y, yaw) == 0;
} catch (const std::exception& e) {
LOG_ERROR("Move to position failed: " + std::string(e.what()));
return false;
}
}
bool Controller::stop() {
try {
return sportClient_ && sportClient_->StopMove() == 0;
} catch (const std::exception& e) {
LOG_ERROR("Stop failed: " + std::string(e.what()));
return false;
}
}
// Body control
bool Controller::setBodyPose(double roll, double pitch, double yaw, double height) {
try {
if (sportClient_) {
sportClient_->Euler(roll, pitch, yaw);
sportClient_->BodyHeight(height);
return true;
}
return false;
} catch (const std::exception& e) {
LOG_ERROR("Set body pose failed: " + std::string(e.what()));
return false;
}
}
bool Controller::balanceStand() {
try {
return sportClient_ && sportClient_->BalanceStand() == 0;
} catch (const std::exception& e) {
LOG_ERROR("Balance stand failed: " + std::string(e.what()));
return false;
}
}
// Special actions
bool Controller::dance1() {
try {
return sportClient_ && sportClient_->Dance1() == 0;
} catch (const std::exception& e) {
LOG_ERROR("Dance1 failed: " + std::string(e.what()));
return false;
}
}
bool Controller::dance2() {
try {
return sportClient_ && sportClient_->Dance2() == 0;
} catch (const std::exception& e) {
LOG_ERROR("Dance2 failed: " + std::string(e.what()));
return false;
}
}
bool Controller::hello() {
try {
return sportClient_ && sportClient_->Hello() == 0;
} catch (const std::exception& e) {
LOG_ERROR("Hello failed: " + std::string(e.what()));
return false;
}
}
// Control loops
void Controller::controlLoop() {
LOG_INFO("Control loop started");
while (running_) {
auto startTime = std::chrono::steady_clock::now();
try {
processCommands();
checkSafetyLimits();
} catch (const std::exception& e) {
LOG_ERROR("Control loop error: " + std::string(e.what()));
handleError(e.what());
}
auto endTime = std::chrono::steady_clock::now();
auto elapsed = endTime - startTime;
if (elapsed < controlPeriod_) {
std::this_thread::sleep_for(controlPeriod_ - elapsed);
}
}
LOG_INFO("Control loop stopped");
}
void Controller::stateUpdateLoop() {
LOG_INFO("State update loop started");
while (running_) {
auto startTime = std::chrono::steady_clock::now();
try {
// State is updated via callbacks, just trigger callback here
if (stateCallback_) {
RobotState state = getCurrentState();
stateCallback_(state);
}
} catch (const std::exception& e) {
LOG_ERROR("State update error: " + std::string(e.what()));
}
auto endTime = std::chrono::steady_clock::now();
auto elapsed = endTime - startTime;
if (elapsed < statePeriod_) {
std::this_thread::sleep_for(statePeriod_ - elapsed);
}
}
LOG_INFO("State update loop stopped");
}
void Controller::processCommands() {
std::unique_lock<std::mutex> lock(commandMutex_);
while (!commandQueue_.empty()) {
auto command = commandQueue_.front();
commandQueue_.pop();
lock.unlock();
executeMotionCommand(command);
lock.lock();
}
}
void Controller::executeMotionCommand(const MotionCommand& command) {
switch (command.type) {
case MotionCommand::VELOCITY:
move(command.linear_velocity[0], command.linear_velocity[1], command.angular_velocity[2]);
break;
case MotionCommand::POSITION:
moveToPosition(command.target_position[0], command.target_position[1], command.target_orientation[2]);
break;
case MotionCommand::BODY_POSE:
setBodyPose(command.body_roll, command.body_pitch, command.body_yaw, command.body_height);
break;
case MotionCommand::SPECIAL_ACTION:
performAction(command.action_name, command.action_params);
break;
default:
LOG_WARN("Unknown motion command type");
break;
}
}
bool Controller::validateCommand(const MotionCommand& command) {
auto& config = Config::getInstance().getConfig();
// Check velocity limits
if (command.type == MotionCommand::VELOCITY) {
double vx = std::abs(command.linear_velocity[0]);
double vy = std::abs(command.linear_velocity[1]);
double vyaw = std::abs(command.angular_velocity[2]);
if (vx > config.max_linear_velocity || vy > config.max_linear_velocity ||
vyaw > config.max_angular_velocity) {
return false;
}
}
return true;
}
void Controller::updateSportModeState(const void* message) {
auto* state = static_cast<const unitree_go::msg::dds_::SportModeState_*>(message);
std::lock_guard<std::mutex> lock(stateMutex_);
// Update position
currentState_.position[0] = state->position()[0];
currentState_.position[1] = state->position()[1];
currentState_.position[2] = state->position()[2];
// Update orientation
currentState_.orientation[0] = state->imu_state().rpy()[0];
currentState_.orientation[1] = state->imu_state().rpy()[1];
currentState_.orientation[2] = state->imu_state().rpy()[2];
// Update velocity
currentState_.velocity[0] = state->velocity()[0];
currentState_.velocity[1] = state->velocity()[1];
currentState_.velocity[2] = state->velocity()[2];
// Update IMU
for (int i = 0; i < 3; i++) {
currentState_.imu_acc[i] = state->imu_state().accelerometer()[i];
currentState_.imu_gyro[i] = state->imu_state().gyroscope()[i];
}
for (int i = 0; i < 4; i++) {
currentState_.imu_quat[i] = state->imu_state().quaternion()[i];
}
currentState_.timestamp = std::chrono::steady_clock::now();
currentState_.is_connected = true;
}
void Controller::updateLowLevelState(const void* message) {
auto* state = static_cast<const unitree_go::msg::dds_::LowState_*>(message);
std::lock_guard<std::mutex> lock(stateMutex_);
// Update motor states
for (int i = 0; i < 20 && i < state->motor_state().size(); i++) {
currentState_.motor_positions[i] = state->motor_state()[i].q();
currentState_.motor_velocities[i] = state->motor_state()[i].dq();
currentState_.motor_torques[i] = state->motor_state()[i].tau_est();
currentState_.motor_temperatures[i] = state->motor_state()[i].temperature();
}
// Update battery info
if (!state->bms_state().empty()) {
currentState_.battery_voltage = state->bms_state()[0].voltage();
currentState_.battery_current = state->bms_state()[0].current();
currentState_.battery_percentage = state->bms_state()[0].soc();
}
currentState_.timestamp = std::chrono::steady_clock::now();
}
void Controller::checkSafetyLimits() {
auto& config = Config::getInstance().getConfig();
auto now = std::chrono::steady_clock::now();
// Check command timeout
auto timeSinceLastCommand = std::chrono::duration_cast<std::chrono::seconds>(
now - lastCommandTime_).count();
if (timeSinceLastCommand > config.emergency_stop_timeout && !emergencyStopActive_) {
LOG_WARN("Command timeout exceeded, activating emergency stop");
emergencyStop();
}
// Check battery level
{
std::lock_guard<std::mutex> lock(stateMutex_);
if (currentState_.battery_percentage < 10) {
currentState_.low_battery = true;
if (!emergencyStopActive_) {
LOG_WARN("Low battery detected, activating emergency stop");
emergencyStop();
}
}
}
}
void Controller::handleError(const std::string& error) {
if (errorCallback_) {
errorCallback_(error);
}
}
bool Controller::performAction(const std::string& actionName, const nlohmann::json& params) {
LOG_INFO("Performing action: " + actionName);
if (actionName == "dance1") return dance1();
if (actionName == "dance2") return dance2();
if (actionName == "hello") return hello();
if (actionName == "sit") return sit();
if (actionName == "stand_up") return standUp();
if (actionName == "stand_down") return standDown();
if (actionName == "damp") return damp();
if (actionName == "balance_stand") return balanceStand();
if (actionName == "recovery_stand") return recoveryStand();
LOG_WARN("Unknown action: " + actionName);
return false;
}
} // namespace custom

682
src/custom_robot.cpp Normal file
View File

@@ -0,0 +1,682 @@
#include "custom_robot.hpp"
#include <chrono>
#include <sstream>
namespace custom {
CustomRobot::CustomRobot(const std::string& configFile)
: configFile_(configFile.empty() ? "config/robot_config.json" : configFile),
running_(false), initialized_(false), mqttConnected_(false) {
stats_.startTime = std::chrono::steady_clock::now();
}
CustomRobot::~CustomRobot() {
shutdown();
}
bool CustomRobot::initialize() {
LOG_INFO("Initializing CustomRobot");
// Load configuration
if (!Config::getInstance().loadConfig(configFile_)) {
LOG_WARN("Failed to load config file, using defaults");
}
auto& config = Config::getInstance().getConfig();
// Initialize robot controller
robotController_ = std::make_unique<Controller>(config.network_interface);
if (!robotController_->initialize()) {
LOG_ERROR("Failed to initialize robot controller");
return false;
}
// Set robot controller callbacks
robotController_->setStateCallback(
std::bind(&CustomRobot::handleRobotState, this, std::placeholders::_1)
);
robotController_->setErrorCallback(
std::bind(&CustomRobot::handleRobotError, this, std::placeholders::_1)
);
// Initialize MQTT client
mqttClient_ = std::make_unique<MqttClient>(
config.mqtt_broker, config.mqtt_port, config.mqtt_client_id
);
// Set MQTT callbacks
mqttClient_->setMessageCallback(
std::bind(&CustomRobot::handleMqttMessage, this, std::placeholders::_1, std::placeholders::_2)
);
mqttClient_->setConnectionCallback(
std::bind(&CustomRobot::handleMqttConnection, this, std::placeholders::_1)
);
// Connect to MQTT broker
if (!mqttClient_->connect(config.mqtt_username, config.mqtt_password)) {
LOG_ERROR("Failed to connect to MQTT broker");
return false;
}
// Start MQTT message processor
mqttClient_->startMessageProcessor();
// Subscribe to command topics
std::string cmdTopic = Config::getInstance().getFullTopic(config.cmd_topic);
if (!mqttClient_->subscribe(cmdTopic + "/+")) { // Subscribe to all command subtopics
LOG_ERROR("Failed to subscribe to command topics");
return false;
}
initialized_ = true;
LOG_INFO("CustomRobot initialized successfully");
return true;
}
void CustomRobot::shutdown() {
LOG_INFO("Shutting down CustomRobot");
stop();
if (mqttClient_) {
mqttClient_->stopMessageProcessor();
mqttClient_->disconnect();
mqttClient_.reset();
}
if (robotController_) {
robotController_->shutdown();
robotController_.reset();
}
initialized_ = false;
}
bool CustomRobot::start() {
if (!initialized_) {
LOG_ERROR("Cannot start: not initialized");
return false;
}
if (running_) {
LOG_WARN("CustomRobot already running");
return true;
}
LOG_INFO("Starting CustomRobot");
// Start robot controller
if (!robotController_->start()) {
LOG_ERROR("Failed to start robot controller");
return false;
}
running_ = true;
// Start periodic tasks
startPeriodicTasks();
// Publish initial status
publishHeartbeat();
LOG_INFO("CustomRobot started successfully");
return true;
}
bool CustomRobot::stop() {
if (!running_) {
return true;
}
LOG_INFO("Stopping CustomRobot");
running_ = false;
// Stop periodic tasks
stopPeriodicTasks();
// Stop robot controller
if (robotController_) {
robotController_->stop();
}
// Publish final status
if (mqttClient_ && mqttClient_->isConnected()) {
nlohmann::json status;
status["status"] = "stopped";
status["timestamp"] = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
auto& config = Config::getInstance().getConfig();
std::string statusTopic = Config::getInstance().getFullTopic(config.state_topic + "/status");
mqttClient_->publishJson(statusTopic, status);
}
LOG_INFO("CustomRobot stopped");
return true;
}
void CustomRobot::run() {
if (!start()) {
LOG_ERROR("Failed to start CustomRobot");
return;
}
LOG_INFO("CustomRobot main loop started");
while (running_) {
try {
// Process any pending errors
processErrorQueue();
// Sleep for a short period
std::this_thread::sleep_for(std::chrono::milliseconds(100));
} catch (const std::exception& e) {
LOG_ERROR("Main loop error: " + std::string(e.what()));
}
}
LOG_INFO("CustomRobot main loop ended");
}
void CustomRobot::runAsync() {
mainThread_ = std::thread(&CustomRobot::run, this);
}
void CustomRobot::waitForShutdown() {
if (mainThread_.joinable()) {
mainThread_.join();
}
}
void CustomRobot::handleMqttMessage(const std::string& topic, const std::string& payload) {
try {
LOG_DEBUG("Received MQTT message on topic: " + topic);
auto& config = Config::getInstance().getConfig();
std::string cmdPrefix = Config::getInstance().getFullTopic(config.cmd_topic);
if (topic.find(cmdPrefix) == 0) {
// Parse JSON payload
nlohmann::json message = nlohmann::json::parse(payload);
// Extract command type from topic
std::string cmdType = topic.substr(cmdPrefix.length() + 1);
if (cmdType == "motion") {
handleCommandMessage(message);
} else if (cmdType == "config") {
handleConfigMessage(message);
} else if (cmdType == "control") {
handleControlMessage(message);
} else {
LOG_WARN("Unknown command type: " + cmdType);
}
stats_.commandsReceived++;
}
} catch (const nlohmann::json::parse_error& e) {
LOG_ERROR("JSON parse error: " + std::string(e.what()));
publishError("Invalid JSON in message: " + std::string(e.what()));
} catch (const std::exception& e) {
LOG_ERROR("Message handling error: " + std::string(e.what()));
publishError("Message handling error: " + std::string(e.what()));
}
}
void CustomRobot::handleCommandMessage(const nlohmann::json& command) {
std::string requestId = command.value("request_id", "");
try {
if (!command.contains("type")) {
publishResponse(requestId, false, "Missing command type");
return;
}
std::string cmdType = command["type"];
bool success = false;
if (cmdType == "motion") {
success = processMotionCommand(command);
} else if (cmdType == "special_action") {
success = processSpecialAction(command);
} else if (cmdType == "system") {
success = processSystemCommand(command);
} else {
publishResponse(requestId, false, "Unknown command type: " + cmdType);
return;
}
if (success) {
stats_.commandsExecuted++;
publishResponse(requestId, true);
} else {
stats_.commandsFailed++;
publishResponse(requestId, false, "Command execution failed");
}
lastCommandReceived_ = std::chrono::steady_clock::now();
} catch (const std::exception& e) {
LOG_ERROR("Command processing error: " + std::string(e.what()));
publishResponse(requestId, false, "Command processing error: " + std::string(e.what()));
stats_.commandsFailed++;
}
}
bool CustomRobot::processMotionCommand(const nlohmann::json& cmd) {
if (!robotController_) {
return false;
}
MotionCommand motionCmd = jsonToMotionCommand(cmd);
return robotController_->executeCommand(motionCmd);
}
bool CustomRobot::processSpecialAction(const nlohmann::json& cmd) {
if (!robotController_ || !cmd.contains("action")) {
return false;
}
std::string action = cmd["action"];
nlohmann::json params = cmd.value("params", nlohmann::json::object());
return robotController_->performAction(action, params);
}
bool CustomRobot::processSystemCommand(const nlohmann::json& cmd) {
if (!cmd.contains("command")) {
return false;
}
std::string sysCmd = cmd["command"];
if (sysCmd == "emergency_stop") {
return robotController_ && robotController_->emergencyStop();
} else if (sysCmd == "recovery_stand") {
return robotController_ && robotController_->recoveryStand();
} else if (sysCmd == "start") {
return robotController_ && robotController_->start();
} else if (sysCmd == "stop") {
return robotController_ && robotController_->stop();
}
return false;
}
void CustomRobot::handleConfigMessage(const nlohmann::json& config) {
// Handle configuration updates
LOG_INFO("Received configuration update");
try {
// Update configuration and save
auto& currentConfig = Config::getInstance().getConfig();
if (config.contains("control_frequency")) {
currentConfig.control_frequency = config["control_frequency"];
}
if (config.contains("state_publish_frequency")) {
currentConfig.state_publish_frequency = config["state_publish_frequency"];
}
// Save updated configuration
Config::getInstance().saveConfig(configFile_);
LOG_INFO("Configuration updated successfully");
} catch (const std::exception& e) {
LOG_ERROR("Configuration update error: " + std::string(e.what()));
}
}
void CustomRobot::handleControlMessage(const nlohmann::json& control) {
// Handle control messages (start/stop/pause etc.)
if (control.contains("command")) {
std::string cmd = control["command"];
if (cmd == "start" && !running_) {
start();
} else if (cmd == "stop" && running_) {
stop();
} else if (cmd == "restart") {
stop();
std::this_thread::sleep_for(std::chrono::seconds(1));
start();
}
}
}
void CustomRobot::handleRobotState(const RobotState& state) {
// Cache the latest state
{
std::lock_guard<std::mutex> lock(stateCacheMutex_);
lastKnownState_ = state;
}
// Publish state if enough time has passed
auto now = std::chrono::steady_clock::now();
auto timeSinceLastPublish = std::chrono::duration<double>(now - lastStatePublish_).count();
if (timeSinceLastPublish >= statePublishInterval_) {
publishState();
lastStatePublish_ = now;
}
}
void CustomRobot::handleRobotError(const std::string& error) {
{
std::lock_guard<std::mutex> lock(errorMutex_);
errorQueue_.push(error);
}
stats_.errorsOccurred++;
}
void CustomRobot::publishState() {
if (!mqttClient_ || !mqttClient_->isConnected()) {
return;
}
try {
RobotState state;
{
std::lock_guard<std::mutex> lock(stateCacheMutex_);
state = lastKnownState_;
}
nlohmann::json stateJson = robotStateToJson(state);
auto& config = Config::getInstance().getConfig();
std::string stateTopic = Config::getInstance().getFullTopic(config.state_topic + "/robot");
mqttClient_->publishJson(stateTopic, stateJson);
stats_.statesPublished++;
} catch (const std::exception& e) {
LOG_ERROR("State publishing error: " + std::string(e.what()));
}
}
void CustomRobot::publishHeartbeat() {
if (!mqttClient_ || !mqttClient_->isConnected()) {
return;
}
try {
nlohmann::json heartbeat = createStatusMessage();
auto& config = Config::getInstance().getConfig();
std::string heartbeatTopic = Config::getInstance().getFullTopic(config.state_topic + "/heartbeat");
mqttClient_->publishJson(heartbeatTopic, heartbeat);
lastHeartbeat_ = std::chrono::steady_clock::now();
} catch (const std::exception& e) {
LOG_ERROR("Heartbeat publishing error: " + std::string(e.what()));
}
}
void CustomRobot::publishError(const std::string& error) {
if (!mqttClient_ || !mqttClient_->isConnected()) {
return;
}
try {
nlohmann::json errorMsg;
errorMsg["error"] = error;
errorMsg["timestamp"] = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
auto& config = Config::getInstance().getConfig();
std::string errorTopic = Config::getInstance().getFullTopic(config.state_topic + "/error");
mqttClient_->publishJson(errorTopic, errorMsg);
} catch (const std::exception& e) {
LOG_ERROR("Error publishing error: " + std::string(e.what()));
}
}
void CustomRobot::publishResponse(const std::string& requestId, bool success, const std::string& message) {
if (!mqttClient_ || !mqttClient_->isConnected() || requestId.empty()) {
return;
}
try {
nlohmann::json response;
response["request_id"] = requestId;
response["success"] = success;
response["message"] = message;
response["timestamp"] = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
auto& config = Config::getInstance().getConfig();
std::string responseTopic = Config::getInstance().getFullTopic(config.state_topic + "/response");
mqttClient_->publishJson(responseTopic, response);
} catch (const std::exception& e) {
LOG_ERROR("Response publishing error: " + std::string(e.what()));
}
}
MotionCommand CustomRobot::jsonToMotionCommand(const nlohmann::json& json) {
MotionCommand cmd;
cmd.timestamp = std::chrono::steady_clock::now();
if (json.contains("motion_type")) {
std::string motionType = json["motion_type"];
if (motionType == "velocity") {
cmd.type = MotionCommand::VELOCITY;
} else if (motionType == "position") {
cmd.type = MotionCommand::POSITION;
} else if (motionType == "body_pose") {
cmd.type = MotionCommand::BODY_POSE;
} else if (motionType == "special_action") {
cmd.type = MotionCommand::SPECIAL_ACTION;
}
}
// Parse velocity command
if (json.contains("linear_velocity")) {
auto vel = json["linear_velocity"];
if (vel.is_array() && vel.size() >= 3) {
cmd.linear_velocity[0] = vel[0];
cmd.linear_velocity[1] = vel[1];
cmd.linear_velocity[2] = vel[2];
}
}
if (json.contains("angular_velocity")) {
auto vel = json["angular_velocity"];
if (vel.is_array() && vel.size() >= 3) {
cmd.angular_velocity[0] = vel[0];
cmd.angular_velocity[1] = vel[1];
cmd.angular_velocity[2] = vel[2];
}
}
// Parse position command
if (json.contains("target_position")) {
auto pos = json["target_position"];
if (pos.is_array() && pos.size() >= 3) {
cmd.target_position[0] = pos[0];
cmd.target_position[1] = pos[1];
cmd.target_position[2] = pos[2];
}
}
// Parse body pose
if (json.contains("body_height")) cmd.body_height = json["body_height"];
if (json.contains("body_roll")) cmd.body_roll = json["body_roll"];
if (json.contains("body_pitch")) cmd.body_pitch = json["body_pitch"];
if (json.contains("body_yaw")) cmd.body_yaw = json["body_yaw"];
// Parse special action
if (json.contains("action_name")) {
cmd.action_name = json["action_name"];
}
if (json.contains("action_params")) {
cmd.action_params = json["action_params"];
}
// Parse duration
if (json.contains("duration")) {
cmd.duration = json["duration"];
}
return cmd;
}
nlohmann::json CustomRobot::robotStateToJson(const RobotState& state) {
nlohmann::json json;
// Basic info
json["timestamp"] = std::chrono::duration_cast<std::chrono::milliseconds>(
state.timestamp.time_since_epoch()).count();
json["is_connected"] = state.is_connected;
json["emergency_stop"] = state.emergency_stop;
// Position and orientation
json["position"] = {state.position[0], state.position[1], state.position[2]};
json["orientation"] = {state.orientation[0], state.orientation[1], state.orientation[2]};
json["velocity"] = {state.velocity[0], state.velocity[1], state.velocity[2]};
// IMU data
json["imu"]["acceleration"] = {state.imu_acc[0], state.imu_acc[1], state.imu_acc[2]};
json["imu"]["gyroscope"] = {state.imu_gyro[0], state.imu_gyro[1], state.imu_gyro[2]};
json["imu"]["quaternion"] = {state.imu_quat[0], state.imu_quat[1], state.imu_quat[2], state.imu_quat[3]};
// Battery info
json["battery"]["voltage"] = state.battery_voltage;
json["battery"]["current"] = state.battery_current;
json["battery"]["percentage"] = state.battery_percentage;
json["battery"]["low_battery"] = state.low_battery;
// System info
json["temperature"] = state.temperature;
json["overheated"] = state.overheated;
// Motor states (first 12 leg motors)
json["motors"]["positions"] = std::vector<double>(state.motor_positions, state.motor_positions + 12);
json["motors"]["velocities"] = std::vector<double>(state.motor_velocities, state.motor_velocities + 12);
json["motors"]["torques"] = std::vector<double>(state.motor_torques, state.motor_torques + 12);
json["motors"]["temperatures"] = std::vector<double>(state.motor_temperatures, state.motor_temperatures + 12);
return json;
}
nlohmann::json CustomRobot::createStatusMessage() {
nlohmann::json status;
status["status"] = running_ ? "running" : "stopped";
status["initialized"] = initialized_;
status["mqtt_connected"] = mqttConnected_;
status["robot_connected"] = robotController_ && robotController_->isRunning();
// Statistics
status["stats"]["commands_received"] = stats_.commandsReceived;
status["stats"]["commands_executed"] = stats_.commandsExecuted;
status["stats"]["commands_failed"] = stats_.commandsFailed;
status["stats"]["states_published"] = stats_.statesPublished;
status["stats"]["errors_occurred"] = stats_.errorsOccurred;
// Uptime
auto now = std::chrono::steady_clock::now();
auto uptime = std::chrono::duration_cast<std::chrono::seconds>(now - stats_.startTime).count();
status["uptime_seconds"] = uptime;
status["timestamp"] = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
return status;
}
void CustomRobot::startPeriodicTasks() {
// Heartbeat thread
periodicThreads_.emplace_back([this]() {
while (running_) {
auto now = std::chrono::steady_clock::now();
auto timeSinceLastHeartbeat = std::chrono::duration<double>(now - lastHeartbeat_).count();
if (timeSinceLastHeartbeat >= heartbeatInterval_) {
publishHeartbeat();
}
std::this_thread::sleep_for(std::chrono::seconds(1));
}
});
// Safety monitor thread
safetyThread_ = std::thread(&CustomRobot::safetyMonitorLoop, this);
}
void CustomRobot::stopPeriodicTasks() {
// Wait for all periodic threads to finish
for (auto& thread : periodicThreads_) {
if (thread.joinable()) {
thread.join();
}
}
periodicThreads_.clear();
if (safetyThread_.joinable()) {
safetyThread_.join();
}
}
void CustomRobot::handleMqttConnection(bool connected) {
mqttConnected_ = connected;
if (connected) {
LOG_INFO("MQTT connection established");
// Resubscribe to topics
auto& config = Config::getInstance().getConfig();
std::string cmdTopic = Config::getInstance().getFullTopic(config.cmd_topic);
mqttClient_->subscribe(cmdTopic + "/+");
// Publish connection status
publishHeartbeat();
} else {
LOG_WARN("MQTT connection lost");
}
}
void CustomRobot::safetyMonitorLoop() {
while (running_) {
try {
checkEmergencyConditions();
std::this_thread::sleep_for(std::chrono::milliseconds(500));
} catch (const std::exception& e) {
LOG_ERROR("Safety monitor error: " + std::string(e.what()));
}
}
}
void CustomRobot::checkEmergencyConditions() {
// Check command timeout
auto now = std::chrono::steady_clock::now();
auto timeSinceLastCommand = std::chrono::duration<double>(now - lastCommandReceived_).count();
if (timeSinceLastCommand > maxIdleTime_ && robotController_ && robotController_->isRunning()) {
LOG_WARN("Maximum idle time exceeded, stopping robot");
robotController_->emergencyStop();
}
}
void CustomRobot::processErrorQueue() {
std::lock_guard<std::mutex> lock(errorMutex_);
while (!errorQueue_.empty()) {
std::string error = errorQueue_.front();
errorQueue_.pop();
publishError(error);
}
}
} // namespace custom

87
src/logger.cpp Normal file
View File

@@ -0,0 +1,87 @@
#include "logger.hpp"
#include <iostream>
namespace custom {
Logger& Logger::getInstance() {
static Logger instance;
return instance;
}
void Logger::setLevel(LogLevel level) {
std::lock_guard<std::mutex> lock(logMutex_);
currentLevel_ = level;
}
void Logger::setLogFile(const std::string& filename) {
std::lock_guard<std::mutex> lock(logMutex_);
if (logFile_) {
logFile_->close();
}
logFile_ = std::make_unique<std::ofstream>(filename, std::ios::app);
if (!logFile_->is_open()) {
std::cerr << "Failed to open log file: " << filename << std::endl;
logFile_.reset();
}
}
void Logger::log(LogLevel level, const std::string& message) {
if (level < currentLevel_) {
return;
}
std::lock_guard<std::mutex> lock(logMutex_);
std::string timestamp = getCurrentTime();
std::string levelStr = levelToString(level);
std::string logMessage = "[" + timestamp + "] [" + levelStr + "] " + message;
// Always log to console
std::cout << logMessage << std::endl;
// Log to file if available
if (logFile_ && logFile_->is_open()) {
*logFile_ << logMessage << std::endl;
logFile_->flush();
}
}
void Logger::debug(const std::string& message) {
log(LogLevel::DEBUG, message);
}
void Logger::info(const std::string& message) {
log(LogLevel::INFO, message);
}
void Logger::warn(const std::string& message) {
log(LogLevel::WARN, message);
}
void Logger::error(const std::string& message) {
log(LogLevel::ERROR, message);
}
std::string Logger::getCurrentTime() {
auto now = std::chrono::system_clock::now();
auto time_t = std::chrono::system_clock::to_time_t(now);
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
now.time_since_epoch()) % 1000;
std::stringstream ss;
ss << std::put_time(std::localtime(&time_t), "%Y-%m-%d %H:%M:%S");
ss << "." << std::setfill('0') << std::setw(3) << ms.count();
return ss.str();
}
std::string Logger::levelToString(LogLevel level) {
switch (level) {
case LogLevel::DEBUG: return "DEBUG";
case LogLevel::INFO: return "INFO ";
case LogLevel::WARN: return "WARN ";
case LogLevel::ERROR: return "ERROR";
default: return "UNKNOWN";
}
}
} // namespace custom

54
src/main.cpp Normal file
View File

@@ -0,0 +1,54 @@
#include <iostream>
#include <memory>
#include <signal.h>
#include "custom_robot.hpp"
#include "logger.hpp"
#include "config.hpp"
using namespace custom;
std::unique_ptr<CustomRobot> g_robot;
void signalHandler(int signal) {
LOG_INFO("Received signal " + std::to_string(signal) + ", shutting down...");
if (g_robot) {
g_robot->stop();
}
exit(0);
}
int main(int argc, char** argv) {
// Initialize logger with default settings
Logger::getInstance().setLevel(LogLevel::INFO);
LOG_INFO("Starting Unitree GO2 System v0.0.1");
// Setup signal handlers
signal(SIGINT, signalHandler);
signal(SIGTERM, signalHandler);
try {
// Create robot with compile-time defaults (no config file needed)
g_robot = std::make_unique<CustomRobot>("");
if (!g_robot->initialize()) {
LOG_ERROR("Failed to initialize robot");
return 1;
}
LOG_INFO("Robot initialized successfully");
LOG_INFO("Press Ctrl+C to stop the robot");
// Run the robot
g_robot->run();
} catch (const std::exception& e) {
LOG_ERROR("Exception: " + std::string(e.what()));
return 1;
} catch (...) {
LOG_ERROR("Unknown exception occurred");
return 1;
}
return 0;
}

265
src/mqtt.cpp Normal file
View File

@@ -0,0 +1,265 @@
#include "mqtt.hpp"
#include "logger.hpp"
#include <sstream>
namespace custom {
MqttClient::MqttClient(const std::string& broker, int port, const std::string& clientId)
: broker_(broker), port_(port), clientId_(clientId),
connected_(false), reconnecting_(false), processorRunning_(false),
shouldReconnect_(true) {
std::stringstream ss;
ss << "tcp://" << broker_ << ":" << port_;
std::string serverURI = ss.str();
client_ = std::make_unique<mqtt::async_client>(serverURI, clientId_);
callback_ = std::make_unique<Callback>(this);
client_->set_callback(*callback_);
}
MqttClient::~MqttClient() {
shouldReconnect_ = false;
stopMessageProcessor();
disconnect();
if (reconnectThread_.joinable()) {
reconnectThread_.join();
}
}
bool MqttClient::connect(const std::string& username, const std::string& password) {
try {
username_ = username;
password_ = password;
mqtt::connect_options connOpts;
connOpts.set_keep_alive_interval(20);
connOpts.set_clean_session(true);
connOpts.set_automatic_reconnect(true);
if (!username_.empty()) {
connOpts.set_user_name(username_);
if (!password_.empty()) {
connOpts.set_password(password_);
}
}
LOG_INFO("Connecting to MQTT broker: " + broker_ + ":" + std::to_string(port_));
auto token = client_->connect(connOpts, nullptr, *callback_);
token->wait_for(std::chrono::seconds(10));
if (token->is_complete() && client_->is_connected()) {
connected_ = true;
LOG_INFO("Connected to MQTT broker successfully");
return true;
} else {
LOG_ERROR("Failed to connect to MQTT broker");
return false;
}
} catch (const mqtt::exception& e) {
LOG_ERROR("MQTT connection exception: " + std::string(e.what()));
return false;
}
}
void MqttClient::disconnect() {
try {
if (client_ && client_->is_connected()) {
LOG_INFO("Disconnecting from MQTT broker");
auto token = client_->disconnect();
token->wait_for(std::chrono::seconds(5));
connected_ = false;
}
} catch (const mqtt::exception& e) {
LOG_ERROR("MQTT disconnect exception: " + std::string(e.what()));
}
}
bool MqttClient::isConnected() const {
return connected_ && client_ && client_->is_connected();
}
bool MqttClient::subscribe(const std::string& topic, int qos) {
try {
if (!isConnected()) {
LOG_ERROR("Cannot subscribe: not connected to MQTT broker");
return false;
}
LOG_INFO("Subscribing to topic: " + topic);
auto token = client_->subscribe(topic, qos, nullptr, *callback_);
token->wait_for(std::chrono::seconds(5));
return token->is_complete();
} catch (const mqtt::exception& e) {
LOG_ERROR("MQTT subscribe exception: " + std::string(e.what()));
return false;
}
}
bool MqttClient::unsubscribe(const std::string& topic) {
try {
if (!isConnected()) {
LOG_ERROR("Cannot unsubscribe: not connected to MQTT broker");
return false;
}
LOG_INFO("Unsubscribing from topic: " + topic);
auto token = client_->unsubscribe(topic, nullptr, *callback_);
token->wait_for(std::chrono::seconds(5));
return token->is_complete();
} catch (const mqtt::exception& e) {
LOG_ERROR("MQTT unsubscribe exception: " + std::string(e.what()));
return false;
}
}
bool MqttClient::publish(const std::string& topic, const std::string& payload, int qos, bool retain) {
try {
if (!isConnected()) {
LOG_ERROR("Cannot publish: not connected to MQTT broker");
return false;
}
auto msg = mqtt::make_message(topic, payload);
msg->set_qos(qos);
msg->set_retained(retain);
auto token = client_->publish(msg, nullptr, *callback_);
// Don't wait for completion to avoid blocking
return true;
} catch (const mqtt::exception& e) {
LOG_ERROR("MQTT publish exception: " + std::string(e.what()));
return false;
}
}
bool MqttClient::publishJson(const std::string& topic, const nlohmann::json& json, int qos, bool retain) {
return publish(topic, json.dump(), qos, retain);
}
void MqttClient::setMessageCallback(MessageCallback callback) {
messageCallback_ = callback;
}
void MqttClient::setConnectionCallback(ConnectionCallback callback) {
connectionCallback_ = callback;
}
void MqttClient::startMessageProcessor() {
if (processorRunning_) {
return;
}
processorRunning_ = true;
messageProcessor_ = std::thread(&MqttClient::processMessageQueue, this);
LOG_INFO("MQTT message processor started");
}
void MqttClient::stopMessageProcessor() {
if (!processorRunning_) {
return;
}
processorRunning_ = false;
queueCondition_.notify_all();
if (messageProcessor_.joinable()) {
messageProcessor_.join();
}
LOG_INFO("MQTT message processor stopped");
}
void MqttClient::processMessageQueue() {
while (processorRunning_) {
std::unique_lock<std::mutex> lock(queueMutex_);
queueCondition_.wait(lock, [this] {
return !messageQueue_.empty() || !processorRunning_;
});
while (!messageQueue_.empty() && processorRunning_) {
auto message = messageQueue_.front();
messageQueue_.pop();
lock.unlock();
if (messageCallback_) {
try {
messageCallback_(message.topic, message.payload);
} catch (const std::exception& e) {
LOG_ERROR("Exception in message callback: " + std::string(e.what()));
}
}
lock.lock();
}
}
}
void MqttClient::handleConnectionLost() {
connected_ = false;
LOG_WARN("MQTT connection lost");
if (connectionCallback_) {
connectionCallback_(false);
}
if (shouldReconnect_ && !reconnecting_) {
reconnecting_ = true;
reconnectThread_ = std::thread(&MqttClient::handleConnectionLost, this);
}
}
void MqttClient::handleConnectionEstablished() {
connected_ = true;
reconnecting_ = false;
LOG_INFO("MQTT connection established");
if (connectionCallback_) {
connectionCallback_(true);
}
}
// Callback implementation
void MqttClient::Callback::connected(const std::string& cause) {
client_->handleConnectionEstablished();
}
void MqttClient::Callback::connection_lost(const std::string& cause) {
LOG_WARN("MQTT connection lost: " + cause);
client_->handleConnectionLost();
}
void MqttClient::Callback::message_arrived(mqtt::const_message_ptr msg) {
QueuedMessage queuedMsg;
queuedMsg.topic = msg->get_topic();
queuedMsg.payload = msg->to_string();
{
std::lock_guard<std::mutex> lock(client_->queueMutex_);
client_->messageQueue_.push(queuedMsg);
}
client_->queueCondition_.notify_one();
}
void MqttClient::Callback::delivery_complete(mqtt::delivery_token_ptr token) {
// Message delivered successfully
}
void MqttClient::Callback::on_failure(const mqtt::token& tok) {
LOG_ERROR("MQTT operation failed");
}
void MqttClient::Callback::on_success(const mqtt::token& tok) {
// Operation completed successfully
}
} // namespace custom