Skip to content

6. 新建并执行作业文件

本期将介绍如何通过 SDK 新建作业文件、写入延时指令,并验证作业运行、暂停、继续、主动停止和自然完成的完整生命周期。

本 Demo 会为每次运行生成一个唯一的作业名称,在控制器中新建对应的 .JBR 文件,并写入 3 条延时指令。作业会执行两次:第一次用于验证运行、暂停、继续和主动停止,第二次重新启动并等待作业自然完成。

⚠️ 使用注意

当前示例只向作业文件写入延时指令,不包含机器人运动指令。运行前仍需确认:

  • 连接配置: 控制器 IP 地址和 SDK 服务端口与实际配置一致
  • 作业占用: 控制器当前没有其他作业处于运行或暂停状态
  • 伺服状态: 伺服已经上电并进入状态 3
  • 控制器模式: 控制器允许切换到运行模式并执行作业
  • 文件保留: 程序不会删除新建的作业文件,运行后可在控制器中检查
  • 指令安全: 如果将延时指令替换为运动指令,必须重新进行点位、坐标系、可达性和现场安全检查

延时指令本身不会驱动机器人运动,但作业执行能力会真实作用于控制器。请勿在已有生产作业运行时执行本 Demo。

1、配置控制器和作业参数

首先配置控制器 IP 地址和 SDK 服务端口。运行前必须确认程序连接的是预期控制器。

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

示例作业默认写入 3 条延时 2 秒的指令。修改指令数量或单条持续时间后,需要同步调整自然完成等待时间,确保总超时时间大于作业的实际执行时间。

cpp
/**
 * @name 客户需根据本 Demo 修改
 * 以下配置决定示例作业的内容及自然完成等待时间;修改作业内容时必须同步确认超时时间。
 * @{
 */
constexpr int TIMER_COMMAND_COUNT = 3;             ///< 写入示例作业的延时指令数量。
constexpr double TIMER_COMMAND_SECONDS = 2.0;      ///< 每条延时指令的持续时间,单位为秒。
constexpr int JOB_COMPLETE_TIMEOUT_SECONDS = 30;  ///< 等待作业自然完成的总超时时间,单位为秒。
/** @} */

状态切换和控制动作的等待参数用于给控制器留出响应时间,也便于观察暂停和停止效果。

cpp
/**
 * @name 建议客户修改
 * 以下配置应结合控制器响应速度和暂停、停止操作的观察需求进行调整。
 * @{
 */
constexpr int STATE_CHANGE_TIMEOUT_SECONDS = 10;  ///< 等待作业状态切换的总超时时间,单位为秒。
constexpr int CONTROL_ACTION_DELAY_MS = 1000;     ///< 确认运行后,执行暂停或停止前的等待时间。
constexpr int PAUSE_HOLD_MS = 1000;               ///< 暂停状态的保持时间,便于观察暂停效果。
/** @} */

查询参数用于控制状态轮询频率和通信容错次数。连接已经断开时会立即停止重试,其他临时查询错误则按配置等待后再次尝试。

cpp
/**
 * @name 可修改也可保留默认值
 * 以下配置控制查询重试、状态轮询和通信容错,默认值适用于一般演示场景。
 * @{
 */
constexpr int POLL_INTERVAL_MS = 200;          ///< 作业状态轮询间隔,单位为毫秒。
constexpr int QUERY_RETRY_INTERVAL_MS = 500;   ///< 单次状态查询失败后的重试间隔,单位为毫秒。
constexpr int MAX_QUERY_FAILURES = 3;          ///< 状态查询允许的最大连续失败次数。
/** @} */

2、封装 SDK 返回值检查和查询重试

作业生命周期会调用多个 SDK 接口。check_sdk_result 统一判断普通控制接口的返回值,并在失败时输出操作名称和错误码。

cpp
/**
 * @brief 统一检查普通 SDK 接口的返回结果。
 * @param[in] result SDK 接口返回的执行结果。
 * @param[in] operation 当前操作名称,用于输出错误信息。
 * @retval true SDK 接口调用成功。
 * @retval false SDK 接口调用失败。
 */
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;
}

状态查询容易受到短暂通信波动影响,因此程序将统一重试策略封装为模板函数 query_with_retry。调用方传入实际查询操作,即可复用相同的失败次数、间隔和断线处理逻辑。

cpp
/**
 * @brief 执行可重试的 SDK 状态查询。
 * @tparam Query 无参数且返回 Result 的可调用对象类型。
 * @param[in] operation 当前查询名称,用于输出错误信息。
 * @param[in] query 实际执行 SDK 查询的可调用对象。
 * @retval true 在允许的重试次数内查询成功。
 * @retval 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 (result == DISCONNECT)
        {
            std::cerr << "控制器连接已经断开,停止重试" << std::endl;
            return false;
        }

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

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

机器人运行状态、控制器模式、伺服状态和当前活动作业使用四个轻量封装函数查询。它们将具体 SDK 调用和对应的操作名称传给 query_with_retry,使上层业务代码不需要重复编写 Lambda 和错误提示。

cpp
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);
    });
}

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

bool query_current_job(SOCKETFD fd, std::string& job_name)
{
    return query_with_retry("获取当前作业文件", [&]() {
        return job_get_current_file(fd, job_name);
    });
}

3、封装作业名称生成和匹配函数

控制器返回的当前作业可能包含目录、.JBR 扩展名或不同的 ASCII 大小写形式。程序通过 to_upper_asciiis_expected_job 统一处理这些差异,再判断当前活动作业是否为本 Demo 创建的作业。

cpp
/**
 * @brief 将字符串中的 ASCII 字符转换为大写。
 */
std::string to_upper_ascii(std::string value)
{
    for (char& character : value)
    {
        character = static_cast<char>(
            std::toupper(static_cast<unsigned char>(character)));
    }
    return value;
}

/**
 * @brief 判断控制器返回的活动作业是否为预期作业。
 */
bool is_expected_job(
    const std::string& current_job,
    const std::string& expected_job)
{
    std::string current_name = current_job;
    const std::size_t separator = current_name.find_last_of("/\\");
    if (separator != std::string::npos)
    {
        current_name = current_name.substr(separator + 1);
    }

    current_name = to_upper_ascii(current_name);
    if (current_name.size() > 4
        && current_name.compare(current_name.size() - 4, 4, ".JBR") == 0)
    {
        current_name.erase(current_name.size() - 4);
    }

    return current_name == to_upper_ascii(expected_job);
}

每次运行都需要创建新的作业文件,因此程序使用当前本地时间和毫秒数生成 16 位作业名称。

cpp
/**
 * @brief 使用当前本地时间生成本次 Demo 的唯一作业名称。
 * @return 由 16 个字母或数字字符组成的作业名称。
 */
std::string make_unique_job_name()
{
    const auto now = std::chrono::system_clock::now();
    const std::time_t now_time = std::chrono::system_clock::to_time_t(now);
    std::tm local_time{};
    localtime_s(&local_time, &now_time);

    const auto milliseconds =
        std::chrono::duration_cast<std::chrono::milliseconds>(
            now.time_since_epoch()) % 1000;

    std::ostringstream name;
    name << "J" << std::put_time(&local_time, "%y%m%d%H%M%S")
         << std::setw(3) << std::setfill('0') << milliseconds.count();
    return name.str();
}

例如,生成的名称格式类似 J260804143012123,控制器中对应的文件为 J260804143012123.JBR

4、封装作业状态等待函数

作业运行、暂停、继续和停止接口返回成功,只表示控制命令已经被控制器接受,不代表状态已经立即切换。程序需要持续查询机器人运行状态,并在运行或暂停时确认当前活动文件确实是预期作业。

wait_for_job_state 封装了状态轮询、活动作业身份确认、未知状态处理和超时退出。这样每个生命周期操作都可以使用同一套确认规则,避免刚发送命令就直接假定状态切换成功。

cpp
/**
 * @brief 等待指定作业进入预期运行状态。
 * @param[in] fd 控制器连接句柄。
 * @param[in] job_name 预期活动作业名称。
 * @param[in] expected_state 需要等待的机器人运行状态码。
 * @param[in] expected_description 预期状态说明,用于输出处理结果。
 * @param[in] timeout 本次等待允许占用的最长时间。
 * @retval true 已确认预期状态;运行或暂停时也已确认活动作业名称。
 * @retval false 查询失败、活动作业不一致、状态异常或等待超时。
 */
bool wait_for_job_state(
    SOCKETFD fd,
    const std::string& job_name,
    int expected_state,
    const std::string& expected_description,
    std::chrono::seconds timeout =
        std::chrono::seconds(STATE_CHANGE_TIMEOUT_SECONDS))
{
    using clock = std::chrono::steady_clock;
    const auto deadline = clock::now() + timeout;

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

        if (running_state == expected_state)
        {
            if (expected_state == 1 || expected_state == 2)
            {
                std::string current_job;
                if (!query_current_job(fd, current_job))
                {
                    return false;
                }

                if (!is_expected_job(current_job, job_name))
                {
                    std::cerr << "当前活动作业不是预期文件,预期:"
                              << job_name << ",实际:" << current_job << std::endl;
                    return false;
                }
            }

            std::cout << "已确认作业文件" << expected_description << std::endl;
            return true;
        }

        if (expected_state == 1 && running_state == 0)
        {
            std::cerr << "作业已停止,未检测到暂停状态" << std::endl;
            return false;
        }

        if (running_state < 0 || running_state > 2)
        {
            std::cerr << "控制器返回未知运行状态:" << running_state << std::endl;
            return false;
        }

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

    std::cerr << "等待作业文件" << expected_description << "超时" << std::endl;
    return false;
}

运行状态码在本 Demo 中的含义为:状态 0 表示停止,状态 1 表示暂停,状态 2 表示运行。

第二次执行作业时,程序需要区分“尚未开始”与“运行后自然停止”。wait_for_job_completion 只有先观察到预期作业进入运行状态,再观察到停止状态时,才判定作业自然完成。

cpp
bool wait_for_job_completion(
    SOCKETFD fd,
    const std::string& job_name,
    std::chrono::seconds timeout =
        std::chrono::seconds(JOB_COMPLETE_TIMEOUT_SECONDS))
{
    using clock = std::chrono::steady_clock;
    const auto deadline = clock::now() + timeout;
    bool running_observed = false;
    bool pause_reported = false;

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

        switch (running_state)
        {
        case 2:
        {
            std::string current_job;
            if (!query_current_job(fd, current_job))
            {
                return false;
            }
            if (!is_expected_job(current_job, job_name))
            {
                std::cerr << "检测到其他作业正在运行,预期:"
                          << job_name << ",实际:" << current_job << std::endl;
                return false;
            }
            running_observed = true;
            pause_reported = false;
            break;
        }

        case 1:
            if (!pause_reported)
            {
                std::cout << "作业处于暂停状态,继续等待完成" << std::endl;
                pause_reported = true;
            }
            break;

        case 0:
            if (running_observed)
            {
                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;
}

5、封装运行模式准备函数

执行作业前,控制器必须处于运行模式。prepare_run_mode 先保存进入 Demo 前的原始模式,再在需要时切换到模式 2,并重新查询确认切换结果。

cpp
bool prepare_run_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 == 2)
    {
        std::cout << "控制器已处于运行模式" << std::endl;
        return true;
    }

    if (!check_sdk_result(set_current_mode(fd, 2), "切换到运行模式"))
    {
        return false;
    }

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

    std::cout << "已切换到运行模式" << std::endl;
    return true;
}

6、新建作业并写入延时指令

create_and_write_job 依次完成新建、打开、身份确认和指令写入。打开作业后再次查询当前活动文件,可以防止后续指令被写入错误的作业。

cpp
bool create_and_write_job(SOCKETFD fd, const std::string& job_name)
{
    if (!check_sdk_result(job_create(fd, job_name), "新建作业文件"))
    {
        return false;
    }
    std::cout << "已新建作业文件:" << job_name << ".JBR" << std::endl;

    if (!check_sdk_result(job_open(fd, job_name), "打开作业文件"))
    {
        return false;
    }

    std::string current_job;
    if (!query_current_job(fd, current_job))
    {
        return false;
    }
    if (!is_expected_job(current_job, job_name))
    {
        std::cerr << "打开的作业文件与预期不一致,预期:"
                  << job_name << ",实际:" << current_job << std::endl;
        return false;
    }

    // 新建文件确定为空,直接从第 1 行写入,并检查每次插入接口的返回值。
    for (int index = 0; index < TIMER_COMMAND_COUNT; ++index)
    {
        const int insert_line = index + 1;
        if (!check_sdk_result(
                job_insert_timer_command(fd, insert_line, TIMER_COMMAND_SECONDS),
                "写入第 " + std::to_string(index + 1) + " 条延时指令"))
        {
            return false;
        }
    }

    std::cout << "作业内容写入完成,共写入 " << TIMER_COMMAND_COUNT
              << " 条延时指令" << std::endl;
    return true;
}

部分控制器固件对刚创建的空作业调用总行数或逐行读取接口时,可能主动断开 SDK 连接。因此本示例明确知道新文件为空,直接从第 1 行开始插入,并通过每次写入接口的返回值确认结果。

7、封装两次作业执行流程

第一次执行用于验证完整控制生命周期:启动作业、确认运行、暂停、确认暂停、继续、确认恢复运行,最后主动停止并确认停止状态。

cpp
bool run_pause_continue_stop_lifecycle(
    SOCKETFD fd,
    const std::string& job_name)
{
    std::cout << "\n开始第一次执行:验证运行、暂停、继续和停止" << std::endl;

    if (!check_sdk_result(job_run(fd, job_name), "执行作业文件"))
        return false;
    if (!wait_for_job_state(fd, job_name, 2, "正在运行"))
        return false;

    std::this_thread::sleep_for(
        std::chrono::milliseconds(CONTROL_ACTION_DELAY_MS));
    if (!check_sdk_result(job_pause(fd), "暂停作业文件"))
        return false;
    if (!wait_for_job_state(fd, job_name, 1, "已暂停"))
        return false;

    std::this_thread::sleep_for(std::chrono::milliseconds(PAUSE_HOLD_MS));
    if (!check_sdk_result(job_continue(fd), "继续运行作业文件"))
        return false;
    if (!wait_for_job_state(fd, job_name, 2, "已继续运行"))
        return false;

    std::this_thread::sleep_for(
        std::chrono::milliseconds(CONTROL_ACTION_DELAY_MS));
    if (!check_sdk_result(job_stop(fd), "停止作业文件"))
        return false;
    if (!wait_for_job_state(fd, job_name, 0, "已停止"))
        return false;

    std::cout << "第一次执行已主动停止,本次不判定为自然完成" << std::endl;
    return true;
}

第二次执行不再主动暂停或停止,而是等待作业从运行状态自然切换到停止状态。

cpp
bool run_to_natural_completion(SOCKETFD fd, const std::string& job_name)
{
    std::cout << "\n开始第二次执行:等待作业自然完成" << std::endl;

    if (!check_sdk_result(job_run(fd, job_name), "重新执行作业文件"))
    {
        return false;
    }
    if (!wait_for_job_state(fd, job_name, 2, "正在运行"))
    {
        return false;
    }

    return wait_for_job_completion(fd, job_name);
}

8、封装异常安全停止函数

流程中任一步骤失败时,作业可能仍处于运行或暂停状态。stop_job_if_active 会先查询当前状态,只有检测到活动作业时才发送停止命令,并等待状态变为 0。

cpp
bool stop_job_if_active(SOCKETFD fd)
{
    int running_state = 0;
    if (!query_running_state(fd, running_state))
    {
        return false;
    }

    if (running_state == 0)
    {
        return true;
    }

    std::cerr << "检测到作业仍处于运行或暂停状态,正在执行安全停止"
              << std::endl;
    if (!check_sdk_result(job_stop(fd), "异常清理:停止作业文件"))
    {
        return false;
    }

    return wait_for_job_state(fd, "", 0, "已安全停止");
}

停止状态不需要检查活动作业名称,因此异常清理时可以传入空名称并等待状态 0。

9、封装完整 Demo 业务流程

run_demo 将前置检查、模式准备、作业创建、两次执行、失败清理和模式恢复组织成完整流程。

cpp
bool run_demo(SOCKETFD fd)
{
    int initial_running_state = 0;
    if (!query_running_state(fd, initial_running_state))
        return false;

    if (initial_running_state != 0)
    {
        std::string current_job;
        if (query_current_job(fd, current_job))
        {
            std::cerr << "控制器当前已有作业运行或暂停:"
                      << current_job << std::endl;
        }
        std::cerr << "为避免干扰现有任务,本示例不会创建或执行新作业" << std::endl;
        return false;
    }

    int servo_state = 0;
    if (!query_servo_state(fd, servo_state))
        return false;
    if (servo_state != 3)
    {
        std::cerr << "伺服未处于运行状态,当前状态:" << servo_state << std::endl;
        std::cerr << "请先完成上电流程" << std::endl;
        return false;
    }

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

    const std::string job_name = make_unique_job_name();
    bool success = create_and_write_job(fd, job_name);

    if (success)
        success = check_sdk_result(job_run_times(fd, 1), "设置作业运行次数为1次");
    if (success)
        success = run_pause_continue_stop_lifecycle(fd, job_name);
    if (success)
        success = run_to_natural_completion(fd, job_name);

    if (!success)
    {
        stop_job_if_active(fd);
    }

    if (original_mode != 2)
    {
        if (!check_sdk_result(
                set_current_mode(fd, original_mode),
                "恢复控制器原始模式"))
        {
            success = false;
        }
    }

    if (success)
    {
        std::cout << "\nDemo执行成功,作业文件保留在控制器中:"
                  << job_name << ".JBR" << std::endl;
    }
    return success;
}

前置检查会拒绝以下情况:

  • 当前已有作业运行或暂停,避免干扰现有任务
  • 伺服状态不是 3,提示先完成上电流程
  • 控制器返回未知模式或无法切换到运行模式

如果作业流程失败,程序会尝试停止仍处于活动状态的作业。无论业务结果如何,只要原始模式不是运行模式,程序都会尝试恢复进入 Demo 前的控制器模式。

10、主程序连接控制器并执行完整流程

主函数在 Windows 平台启用 UTF-8 控制台输入和输出,然后连接控制器并调用 run_demo。业务成功时返回 0,连接或流程执行失败时返回 1。

cpp
#include <chrono>
#include <cctype>
#include <ctime>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <string>
#include <thread>
#include <cpp_interface/nrc_interface.h>
#include <cpp_interface/nrc_job_operate.h>
#include "../demo_utils.h"

/**
 * @brief Demo 程序入口:连接控制器并执行完整作业生命周期示例。
 * @return Demo 成功时返回 0;连接或执行失败时返回 1。
 */
int main()
{
    demo::enable_console_utf8();

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

    const bool success = run_demo(fd);
    return success ? 0 : 1;
}

程序执行成功后,新建的 .JBR 作业文件会保留在控制器中,便于检查文件内容和执行结果。本示例没有删除作业文件,也没有在 main 返回前显式调用断开接口;集成到实际项目时,应结合连接管理策略主动调用 disconnect_robot,并检查断开结果。