6. 新建并执行作业文件
本期将介绍如何通过 SDK 新建作业文件、写入延时指令,并验证作业运行、暂停、继续、主动停止和自然完成的完整生命周期。
本 Demo 会为每次运行生成一个唯一的作业名称,在控制器中新建对应的 .JBR 文件,并写入 3 条延时指令。作业会执行两次:第一次用于验证运行、暂停、继续和主动停止,第二次重新启动并等待作业自然完成。
⚠️ 使用注意
当前示例只向作业文件写入延时指令,不包含机器人运动指令。运行前仍需确认:
- 连接配置: 控制器 IP 地址和 SDK 服务端口与实际配置一致
- 作业占用: 控制器当前没有其他作业处于运行或暂停状态
- 伺服状态: 伺服已经上电并进入状态 3
- 控制器模式: 控制器允许切换到运行模式并执行作业
- 文件保留: 程序不会删除新建的作业文件,运行后可在控制器中检查
- 指令安全: 如果将延时指令替换为运动指令,必须重新进行点位、坐标系、可达性和现场安全检查
延时指令本身不会驱动机器人运动,但作业执行能力会真实作用于控制器。请勿在已有生产作业运行时执行本 Demo。
1、配置控制器和作业参数
首先配置控制器 IP 地址和 SDK 服务端口。运行前必须确认程序连接的是预期控制器。
/**
* @name 用户必须根据运行环境修改
* 以下配置决定 SDK 能否连接到实际控制器,运行前必须核对。
* @{
*/
const std::string robot_ip = "192.168.3.243"; ///< 实际控制器的 IP 地址。
const std::string robot_port = "6001"; ///< 实际控制器的 SDK 服务端口,必须与控制器配置一致。
/** @} */示例作业默认写入 3 条延时 2 秒的指令。修改指令数量或单条持续时间后,需要同步调整自然完成等待时间,确保总超时时间大于作业的实际执行时间。
/**
* @name 客户需根据本 Demo 修改
* 以下配置决定示例作业的内容及自然完成等待时间;修改作业内容时必须同步确认超时时间。
* @{
*/
constexpr int TIMER_COMMAND_COUNT = 3; ///< 写入示例作业的延时指令数量。
constexpr double TIMER_COMMAND_SECONDS = 2.0; ///< 每条延时指令的持续时间,单位为秒。
constexpr int JOB_COMPLETE_TIMEOUT_SECONDS = 30; ///< 等待作业自然完成的总超时时间,单位为秒。
/** @} */状态切换和控制动作的等待参数用于给控制器留出响应时间,也便于观察暂停和停止效果。
/**
* @name 建议客户修改
* 以下配置应结合控制器响应速度和暂停、停止操作的观察需求进行调整。
* @{
*/
constexpr int STATE_CHANGE_TIMEOUT_SECONDS = 10; ///< 等待作业状态切换的总超时时间,单位为秒。
constexpr int CONTROL_ACTION_DELAY_MS = 1000; ///< 确认运行后,执行暂停或停止前的等待时间。
constexpr int PAUSE_HOLD_MS = 1000; ///< 暂停状态的保持时间,便于观察暂停效果。
/** @} */查询参数用于控制状态轮询频率和通信容错次数。连接已经断开时会立即停止重试,其他临时查询错误则按配置等待后再次尝试。
/**
* @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 统一判断普通控制接口的返回值,并在失败时输出操作名称和错误码。
/**
* @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。调用方传入实际查询操作,即可复用相同的失败次数、间隔和断线处理逻辑。
/**
* @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 和错误提示。
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_ascii 和 is_expected_job 统一处理这些差异,再判断当前活动作业是否为本 Demo 创建的作业。
/**
* @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 位作业名称。
/**
* @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 封装了状态轮询、活动作业身份确认、未知状态处理和超时退出。这样每个生命周期操作都可以使用同一套确认规则,避免刚发送命令就直接假定状态切换成功。
/**
* @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 只有先观察到预期作业进入运行状态,再观察到停止状态时,才判定作业自然完成。
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,并重新查询确认切换结果。
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 依次完成新建、打开、身份确认和指令写入。打开作业后再次查询当前活动文件,可以防止后续指令被写入错误的作业。
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、封装两次作业执行流程
第一次执行用于验证完整控制生命周期:启动作业、确认运行、暂停、确认暂停、继续、确认恢复运行,最后主动停止并确认停止状态。
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;
}第二次执行不再主动暂停或停止,而是等待作业从运行状态自然切换到停止状态。
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。
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 将前置检查、模式准备、作业创建、两次执行、失败清理和模式恢复组织成完整流程。
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。
#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,并检查断开结果。