Skip to content

7. 无作业文件运行

本期将介绍如何在不创建、不打开也不执行作业文件的情况下,通过 SDK 直接向机器人下发 MOVJ 运动指令。

程序会先校验本地运动配置,再连接控制器并完成伺服上电、停止状态确认和远程模式切换。目标点通过可达性预检后,程序直接调用 robot_movej 下发运动指令,等待运动完成并显示最终关节位置,最后在安全条件满足时恢复控制器原始模式并断开连接。

本 Demo 与作业文件运行方式的主要区别如下:

  • 不调用 job_createjob_openjob_run
  • 不在控制器中生成 .JBR 作业文件
  • 运动参数由上位机通过 MoveCmd 直接传递给 robot_movej
  • 上位机负责完成上电、模式切换、预检、运动等待和连接清理

⚠️ 安全警告

以下代码会向控制器发送真实 MOVJ 指令,并导致机器人实际运动。执行前必须确认:

  • 目标点来源: MOVJ_TARGET 必须来自当前机器人实际示教,禁止直接使用源码中的示例坐标控制其他机器人
  • 人员安全: 机器人工作空间和预期路径内无人员、工装干涉或障碍物
  • 参数安全: 速度、加速度和减速度适合当前机器人、负载及调试阶段
  • 控制模式: 控制器允许 SDK 切换到远程模式并直接下发运动指令
  • 可达性限制: 可达性预检只能判断运动学可达,不能代替碰撞、干涉、奇异位形和现场安全检查
  • 急停可用: 急停按钮处于可用状态,并在操作人员可触及范围内

建议先使用全零占位配置并保持确认标志为 false,完成仿真、示教和现场安全验证后,再填写真实目标点并启用运行。

1、配置控制器和目标关节点

首先配置控制器 IP 地址和 SDK 服务端口。程序只有连接到正确的控制器,才能安全执行后续上电和运动流程。

cpp
/**
 * @name 用户必须根据运行环境修改
 * 以下配置决定 SDK 能否连接到实际控制器,运行前必须核对。
 * @{
 */
const std::string robot_ip = "192.168.3.243";  ///< 实际控制器的 IP 地址。
const std::string robot_port = "6001";         ///< 实际控制器的 SDK 服务端口,必须与控制器配置一致。
/** @} */

MOVJ_TARGET 保存七维关节目标数据,TARGET_POSITION_CONFIRMED 是人工安全确认标志。只有目标点来自当前机器人实际示教,并且已经完成路径和现场安全检查后,才能将确认标志设为 true

cpp
/**
 * @name 客户需根据本 Demo 修改
 * 目标点必须来自当前机器人实际示教;确认点位和现场安全后,才可启用确认标志。
 * @{
 */
const std::array<double, 7> MOVJ_TARGET = {
    31.752, -33.184, 31.453, 5.255, -45.787, 31.997, 0.0
};  ///< MOVJ 实际示教关节目标点。

constexpr bool TARGET_POSITION_CONFIRMED = true;  ///< 仅在目标点完成示教和安全确认后设为 true。
/** @} */

源码中的非零坐标只能视为当前示例环境的数据,不能直接用于其他机器人。未填写真实示教点前,应使用以下安全占位格式:

cpp
const std::array<double, 7> MOVJ_TARGET = {
    0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
};
constexpr bool TARGET_POSITION_CONFIRMED = false;

2、配置运动和等待参数

MOVJ 速度、加速度和减速度均使用百分比配置。首次调试应使用较低参数,再根据机器人型号、负载和现场条件逐步调整。

POSITION_DISPLAY_TOLERANCE 只用于运动完成后的偏差提示,不参与运动成功或失败判定。

cpp
/**
 * @name 建议客户修改
 * 首次调试建议使用较低速度和较缓加减速度,并按机器人精度调整位置显示容差。
 * @{
 */
constexpr double MOVJ_VELOCITY = 10.0;              ///< MOVJ 关节运动速度百分比。
constexpr double MOTION_ACCELERATION = 20.0;        ///< 运动加速度百分比。
constexpr double MOTION_DECELERATION = 20.0;        ///< 运动减速度百分比。
constexpr double POSITION_DISPLAY_TOLERANCE = 0.5;  ///< 目标位置与实际位置的显示偏差阈值,单位为度。
/** @} */

等待参数用于限制伺服上电和运动完成的最长等待时间,同时控制状态轮询频率与通信重试次数。

cpp
/**
 * @name 可修改也可保留默认值
 * 以下配置控制等待时间、状态轮询和通信容错,默认值适用于一般演示场景。
 * @{
 */
constexpr int POWER_ON_TIMEOUT_SECONDS = 90;   ///< 等待伺服上电完成的总超时时间,单位为秒。
constexpr int MOTION_TIMEOUT_SECONDS = 30;     ///< 等待单次运动完成的总超时时间,单位为秒。
constexpr int POLL_INTERVAL_MS = 100;          ///< 伺服及运动状态轮询间隔,单位为毫秒。
constexpr int QUERY_RETRY_INTERVAL_MS = 500;   ///< 状态查询失败后的重试间隔,单位为毫秒。
constexpr int MAX_QUERY_FAILURES = 3;          ///< 状态查询允许的最大连续失败次数。
constexpr int STOPPED_CONFIRMATION_COUNT = 5;  ///< 未观察到运行状态时,判定完成所需的连续停止状态次数。
/** @} */

远程模式和关节坐标类型由 SDK 接口语义决定,通常不应修改:

cpp
constexpr int REMOTE_MODE = 1;  ///< SDK 直接控制使用的远程模式。
constexpr int JOINT_COORD = 0;  ///< MOVJ 及关节位置查询使用的关节坐标类型。

3、封装 SDK 返回值检查和状态查询重试

程序中的控制接口统一使用 check_sdk_result 检查返回值,状态查询则通过 query_with_retry 处理短暂通信失败。

cpp
bool check_sdk_result(Result result, const std::string& operation)
{
    if (result == SUCCESS)
    {
        return true;
    }

    std::cerr << operation << "失败,错误码:"
              << static_cast<int>(result) << std::endl;
    return false;
}

template <typename Query>
bool query_with_retry(const std::string& operation, Query query)
{
    for (int attempt = 1; attempt <= MAX_QUERY_FAILURES; ++attempt)
    {
        const Result result = query();
        if (result == SUCCESS)
        {
            return true;
        }

        std::cerr << operation << "失败,第 " << attempt << "/"
                  << MAX_QUERY_FAILURES << " 次,错误码:"
                  << static_cast<int>(result) << std::endl;

        if (attempt < MAX_QUERY_FAILURES)
        {
            std::this_thread::sleep_for(
                std::chrono::milliseconds(QUERY_RETRY_INTERVAL_MS));
        }
    }

    std::cerr << operation << "连续失败,停止当前流程" << std::endl;
    return false;
}

伺服状态、机器人运行状态和控制器模式分别使用轻量包装函数查询。这些函数复用相同的重试策略,并为每类查询提供明确的错误提示。

cpp
bool query_servo_state(SOCKETFD fd, int& state)
{
    return query_with_retry("获取伺服状态", [&]() {
        return get_servo_state(fd, state);
    });
}

bool query_running_state(SOCKETFD fd, int& state)
{
    return query_with_retry("获取机器人运行状态", [&]() {
        return get_robot_running_state(fd, state);
    });
}

bool query_current_mode(SOCKETFD fd, int& mode)
{
    return query_with_retry("获取机器人当前模式", [&]() {
        return get_current_mode(fd, mode);
    });
}

4、封装本地运动配置校验函数

连接控制器和改变伺服状态之前,程序先检查人工确认标志、速度和加减速度范围,以及目标点中是否存在无穷大或非数字值。

cpp
/**
 * @brief 校验目标点确认标志、运动参数范围和目标点数值有效性。
 * @retval true 本地运动配置通过校验。
 * @retval false 目标点未确认、运动参数越界或目标点包含非法数值。
 */
bool validate_motion_configuration()
{
    if (!TARGET_POSITION_CONFIRMED)
    {
        std::cerr << "目标点尚未确认:请填写实际示教关节点,并将 "
                  << "TARGET_POSITION_CONFIRMED 设置为 true" << std::endl;
        return false;
    }

    if (MOVJ_VELOCITY <= 0.0 || MOVJ_VELOCITY > 100.0)
    {
        std::cerr << "MOVJ速度必须位于 (0, 100] 范围内" << std::endl;
        return false;
    }

    if (MOTION_ACCELERATION <= 0.0 || MOTION_ACCELERATION > 100.0
        || MOTION_DECELERATION <= 0.0 || MOTION_DECELERATION > 100.0)
    {
        std::cerr << "加速度和减速度必须位于 (0, 100] 范围内" << std::endl;
        return false;
    }

    for (double value : MOVJ_TARGET)
    {
        if (!std::isfinite(value))
        {
            std::cerr << "目标点包含非法数值" << std::endl;
            return false;
        }
    }

    return true;
}

该函数只验证本地数据格式和参数范围,不能证明目标点属于当前机器人,也不能替代关节限位、可达性和碰撞风险检查。

5、封装伺服上电和状态确认函数

set_servo_poweron 调用成功只表示上电命令已发送,不代表伺服已经立即进入状态 3。wait_until_servo_running 会持续查询伺服状态,直到确认进入运行状态、检测到报警或未知状态,或者等待超时。

cpp
bool wait_until_servo_running(
    SOCKETFD fd,
    std::chrono::seconds timeout =
        std::chrono::seconds(POWER_ON_TIMEOUT_SECONDS))
{
    using clock = std::chrono::steady_clock;
    const auto deadline = clock::now() + timeout;
    int last_state = -1;

    while (clock::now() < deadline)
    {
        if (!query_servo_state(fd, last_state))
            return false;

        if (last_state == 3)
        {
            std::cout << "伺服上电完成,已进入运行状态" << std::endl;
            return true;
        }
        if (last_state == 2)
        {
            std::cerr << "伺服在上电过程中进入报警状态" << std::endl;
            return false;
        }
        if (last_state != 0 && last_state != 1)
        {
            std::cerr << "控制器返回未知伺服状态:" << last_state << std::endl;
            return false;
        }

        std::this_thread::sleep_for(
            std::chrono::milliseconds(POLL_INTERVAL_MS));
    }

    std::cerr << "等待伺服上电超时,最后状态:" << last_state << std::endl;
    return false;
}

power_on 根据当前伺服状态选择对应的上电路径:状态 0 先切换到就绪再上电,状态 1 直接上电,状态 3 直接返回成功。

cpp
bool power_on(SOCKETFD fd)
{
    int servo_state = -1;
    if (!query_servo_state(fd, servo_state))
        return false;

    switch (servo_state)
    {
    case 0:
        if (!check_sdk_result(set_servo_state(fd, 1), "设置伺服为就绪状态"))
            return false;
        if (!check_sdk_result(set_servo_poweron(fd), "机器人上电"))
            return false;
        break;

    case 1:
        if (!check_sdk_result(set_servo_poweron(fd), "机器人上电"))
            return false;
        break;

    case 2:
        std::cerr << "伺服处于报警状态,请排除故障并确认安全后再运行"
                  << std::endl;
        return false;

    case 3:
        std::cout << "伺服已经处于运行状态,无需重复上电" << std::endl;
        return true;

    default:
        std::cerr << "控制器返回未知伺服状态:" << servo_state << std::endl;
        return false;
    }

    return wait_until_servo_running(fd);
}

6、封装远程模式切换函数

SDK 直接下发 MOVJ 需要控制器处于远程模式。switch_to_remote_mode 先保存原始模式,再在需要时切换到模式 1,并重新查询确认切换结果。

cpp
bool switch_to_remote_mode(SOCKETFD fd, int& original_mode)
{
    if (!query_current_mode(fd, original_mode))
        return false;

    if (original_mode < 0 || original_mode > 2)
    {
        std::cerr << "控制器返回未知模式:" << original_mode << std::endl;
        return false;
    }

    if (original_mode == REMOTE_MODE)
    {
        std::cout << "控制器已处于远程模式" << std::endl;
        return true;
    }

    if (!check_sdk_result(
            set_current_mode(fd, REMOTE_MODE),
            "切换控制器到远程模式"))
        return false;

    int confirmed_mode = -1;
    if (!query_current_mode(fd, confirmed_mode))
        return false;
    if (confirmed_mode != REMOTE_MODE)
    {
        std::cerr << "远程模式确认失败,当前模式:" << confirmed_mode << std::endl;
        return false;
    }

    std::cout << "控制器已切换到远程模式" << std::endl;
    return true;
}

7、构造 MOVJ 指令并执行可达性预检

make_movej_command 使用已确认的目标点和运动参数填充 MoveCmd。单独封装构造过程可以集中维护坐标类型、速度、加减速度、平滑参数及构型等字段,避免主流程遗漏配置。

cpp
MoveCmd make_movej_command()
{
    MoveCmd command;
    command.targetPosType = PosType::data;
    std::copy(MOVJ_TARGET.begin(), MOVJ_TARGET.end(), command.targetPosValue.begin());
    command.coord = JOINT_COORD;
    command.velocity = MOVJ_VELOCITY;
    command.acc = MOTION_ACCELERATION;
    command.dec = MOTION_DECELERATION;
    command.pl = 0;
    command.toolNum = 0;
    command.userNum = 0;
    command.configuration = 0;
    return command;
}

可达性接口需要长度为 14 的位置数据,因此 make_reachability_position 将实际 MOVJ 指令转换为接口格式。

cpp
std::vector<double> make_reachability_position(const MoveCmd& command)
{
    std::vector<double> position(14, 0.0);
    position[0] = static_cast<double>(command.coord);
    position[1] = 0.0;  // 0 表示目标点使用角度制。
    position[2] = static_cast<double>(command.configuration);
    position[3] = static_cast<double>(command.toolNum);
    position[4] = static_cast<double>(command.userNum);
    std::copy_n(command.targetPosValue.begin(), 7, position.begin() + 7);
    return position;
}

precheck_reachability 统一调用 get_pos_reachable、检查 SDK 返回值并解释可达结果。预检失败或目标点不可达时,程序不会发送运动指令。

cpp
bool precheck_reachability(SOCKETFD fd, const MoveCmd& command)
{
    bool reachable = false;
    if (!check_sdk_result(
            get_pos_reachable(
                fd, make_reachability_position(command), "MOVJ", reachable),
            "MOVJ目标点可达性预检"))
    {
        return false;
    }

    if (!reachable)
    {
        std::cerr << "MOVJ目标点不可达,已取消运动" << std::endl;
        return false;
    }

    std::cout << "MOVJ目标点可达性预检通过" << std::endl;
    return true;
}

可达性通过只表示目标点在运动学上可达,不能证明实际路径没有碰撞或干涉风险。

8、封装运动完成等待和最终位置显示

robot_movej 返回成功只表示运动指令已经下发。wait_for_motion_complete 持续查询机器人运行状态,并处理运行、暂停、停止、未知状态和超时。

cpp
bool wait_for_motion_complete(
    SOCKETFD fd,
    std::chrono::seconds timeout =
        std::chrono::seconds(MOTION_TIMEOUT_SECONDS))
{
    using clock = std::chrono::steady_clock;
    const auto deadline = clock::now() + timeout;
    bool running_observed = false;
    bool pause_reported = false;
    int stopped_count = 0;

    while (clock::now() < deadline)
    {
        int running_state = -1;
        if (!query_running_state(fd, running_state))
            return false;

        switch (running_state)
        {
        case 2:
            running_observed = true;
            pause_reported = false;
            stopped_count = 0;
            break;

        case 1:
            stopped_count = 0;
            if (!pause_reported)
            {
                std::cout << "机器人运动已暂停,继续等待恢复" << std::endl;
                pause_reported = true;
            }
            break;

        case 0:
            ++stopped_count;
            if (running_observed
                || stopped_count >= STOPPED_CONFIRMATION_COUNT)
            {
                std::cout << "已确认机器人运动完成" << std::endl;
                return true;
            }
            break;

        default:
            std::cerr << "控制器返回未知运行状态:" << running_state << std::endl;
            return false;
        }

        std::this_thread::sleep_for(
            std::chrono::milliseconds(POLL_INTERVAL_MS));
    }

    std::cerr << "等待机器人运动完成超时,请立即确认机器人状态,必要时按下急停"
              << std::endl;
    return false;
}

运动完成后,show_final_position 查询当前关节位置并逐轴显示。单独封装该函数可以统一检查返回容器长度和位置偏差,避免读取不完整数据。

cpp
bool show_final_position(SOCKETFD fd)
{
    std::vector<double> current_position;
    if (!query_with_retry("获取运动完成后的关节位置", [&]() {
            return get_current_position(fd, JOINT_COORD, current_position);
        }))
    {
        return false;
    }

    if (current_position.size() < MOVJ_TARGET.size())
    {
        std::cerr << "控制器返回的关节位置长度不足,实际长度:"
                  << current_position.size() << std::endl;
        return false;
    }

    bool within_tolerance = true;
    std::cout << "运动完成后的关节位置:";
    for (std::size_t index = 0; index < MOVJ_TARGET.size(); ++index)
    {
        if (index != 0)
            std::cout << ", ";
        std::cout << current_position[index];

        if (std::fabs(current_position[index] - MOVJ_TARGET[index])
            > POSITION_DISPLAY_TOLERANCE)
        {
            within_tolerance = false;
        }
    }
    std::cout << std::endl;

    if (!within_tolerance)
    {
        std::cout << "提示:实际关节位置与目标值存在超过 "
                  << POSITION_DISPLAY_TOLERANCE
                  << " 度的偏差,请结合机器人精度和轴数量检查"
                  << std::endl;
    }
    return true;
}

偏差超过阈值只输出提示,不会将本次位置查询判定为失败。该阈值不是控制器到位判定参数。

9、封装无作业文件运动主流程

run_demo 负责组织全部控制步骤,并在每个关键节点执行安全确认:

  1. 根据当前伺服状态完成上电。
  2. 确认机器人当前处于停止状态,避免干扰已有运动。
  3. 保存原模式并切换到远程模式。
  4. 构造 MOVJ 指令并执行可达性预检。
  5. 通过 robot_movej 直接下发运动,不创建作业文件。
  6. 等待运动完成并显示最终关节位置。
  7. 再次查询运行状态,只有确认停止时才恢复原模式。
cpp
bool run_demo(SOCKETFD fd)
{
    if (!power_on(fd))
    {
        std::cerr << "机器人上电失败,不会下发运动指令" << std::endl;
        return false;
    }

    int running_state = -1;
    if (!query_running_state(fd, running_state))
        return false;
    if (running_state != 0)
    {
        std::cerr << "机器人当前不是停止状态,运行状态码:"
                  << running_state << std::endl;
        std::cerr << "为避免干扰现有运动,本示例不会下发新指令" << std::endl;
        return false;
    }

    int original_mode = -1;
    if (!switch_to_remote_mode(fd, original_mode))
        return false;

    bool success = true;
    const MoveCmd command = make_movej_command();

    if (!precheck_reachability(fd, command))
    {
        success = false;
    }
    else if (!check_sdk_result(
                 robot_movej(fd, command),
                 "直接下发MOVJ运动指令"))
    {
        success = false;
    }
    else
    {
        std::cout << "MOVJ指令已通过SDK直接下发,未创建或使用作业文件"
                  << std::endl;
        success = wait_for_motion_complete(fd);
        if (success)
        {
            success = show_final_position(fd);
        }
    }

    int final_running_state = -1;
    if (!query_running_state(fd, final_running_state))
    {
        success = false;
    }
    else if (final_running_state == 0 && original_mode != REMOTE_MODE)
    {
        if (!check_sdk_result(
                set_current_mode(fd, original_mode),
                "恢复控制器原始模式"))
        {
            success = false;
        }
    }
    else if (final_running_state != 0)
    {
        std::cerr << "机器人仍可能处于运动或暂停状态,未自动切换控制器模式"
                  << std::endl;
        success = false;
    }

    return success;
}

当最终运行状态不确定,或者机器人仍在运行或暂停时,程序不会自动切换控制器模式,而是保留远程模式并返回失败,提示操作人员先确认机器人实际状态。

10、主程序校验配置、连接并断开控制器

主函数在连接控制器之前调用 validate_motion_configuration,避免无效配置改变真实控制器状态。连接成功后执行无作业运动流程,无论运动流程成功还是失败,最后都会调用 disconnect_robot 并检查断开结果。

cpp
#include <algorithm>
#include <array>
#include <chrono>
#include <cmath>
#include <iostream>
#include <string>
#include <thread>
#include <vector>

#include <cpp_interface/nrc_interface.h>
#include "../demo_utils.h"

/**
 * @brief Demo 程序入口:校验配置、连接控制器并执行无作业文件的 MOVJ 运动。
 * @return Demo 成功且连接正常断开时返回 0,否则返回 1。
 */
int main()
{
    demo::enable_console_utf8();

    // 在建立连接和上电前验证本地配置,避免使用占位点位改变控制器状态。
    if (!validate_motion_configuration())
    {
        return 1;
    }

    const SOCKETFD fd = connect_robot(robot_ip, robot_port);
    if (fd <= 0)
    {
        std::cerr << "控制器连接失败" << std::endl;
        return 1;
    }
    std::cout << "控制器连接成功" << std::endl;

    bool success = run_demo(fd);

    if (!check_sdk_result(disconnect_robot(fd), "断开控制器连接"))
    {
        success = false;
    }
    else
    {
        std::cout << "控制器连接已断开" << std::endl;
    }

    return success ? 0 : 1;
}