Isaac SDK入门教程-利用C++开发Codelets
Isaac SDK入门教程-利用C++开发Codelets
说明:
- 本教程的目标是在C++中开发一个有效的“ping”机器的codelet。
- 对于本教程,不需要外部依赖项或特殊硬件。
- 创建包
- 首先创建一个新包,方法是在isaac/packages目录中使用类似如下的命令创建一个文件夹:
bob@desktop:~/isaac/packages/$ mkdir ping
- 对于本教程的其余部分,每当要求您创建新文件时,都会将其直接放入此文件夹中。
- 更复杂的包将具有子文件夹,但是对于本教程,保持简单。
- 每个Isaac应用程序都基于JSON文件。 JSON文件描述了应用程序,节点图和消息流的依赖关系,并包含自定义配置。
- 创建一个名为ping.app.json的新JSON文件,并指定其名称,如以下代码段所示:
{
"name": "ping"
}
- 接下来,创建一个bazel构建文件,用于编译和运行应用程序。
- Bazel为大型项目提供了非常好的依赖管理和出色的构建速度,而且bazel构建文件非常容易编写。
- 使用名为ping的新应用程序目标创建文件BUILD,如下所示:
load("//engine/build:isaac.bzl", "isaac_app", "isaac_cc_module")
isaac_app(
name = "ping"
)
- 现在,您可以通过在ping目录中运行以下命令来构建应用程序。
- 从现在开始,当您被要求执行命令时,请在ping目录中执行它。
bob@desktop:~/isaac/packages/ping$ bazel build ping
- 该命令可能需要一些时间,因为下载和编译了Isaac Robot Engine的所有外部依赖项。
- 一段时间后,第一个构建应该成功输出类似于以下内容:
bob@desktop:~/isaac/packages/ping$ bazel build ping
Starting local Bazel server and connecting to it...
INFO: Analysed target //packages/ping:ping (54 packages loaded, 2821 targets configured).
INFO: Found 1 target...
Target //packages/ping:ping up-to-date:
bazel-genfiles/packages/ping/run_ping
bazel-bin/ping/packages/ping
INFO: Elapsed time: 112.170s, Critical Path: 30.14s, Remote (0.00% of the time):
[queue: 0.00%, setup: 0.00%, process: 0.00%]
INFO: 691 processes: 662 linux-sandbox, 29 local.
INFO: Build completed successfully, 1187 total actions
- 接下来,您可以通过执行以下命令来运行新应用程序:
bob@desktop:~/isaac/packages/ping$ bazel run ping
- 这将启动ping应用程序并使其保持运行。
- 您可以通过在控制台中按Ctrl + C来停止正在运行的应用程序。
- 这将优雅地关闭应用程序。
- 您会注意到没有太多事情发生,因为我们还没有应用程序图。
- 接下来,我们将为应用程序创建一些节点。
- 创建节点
Isaac应用程序由许多并行运行的节点组成。
他们可以使用Isaac Robot Engine提供的各种其他机制相互发送消息或相互交互。
节点轻量级的,不需要自己的进程,甚至不需要自己的线程。
要自定义ping节点的行为,我们必须为其配备组件。
我们将创建一个名为Ping的自己的组件。
在ping目录中创建一个新文件Ping.hpp,其中包含以下内容:
#pragma once
#include "engine/alice/alice_codelet.hpp"
class Ping : public isaac::alice::Codelet {
public:
void start() override;
void tick() override;
void stop() override;
};
ISAAC_ALICE_REGISTER_CODELET(Ping);
Codelet提供了三个可以重载的主要功能:start,tick和stop。
启动节点时,首先调用所有附加的codelet的启动函数。
例如,start是分配资源的好地方。
您可以将codelet配置为定期运行tick或每次收到新消息时运行tick
然后,大多数功能由tick函数执行。
在节点停止时,调用停止功能。
您应该在stop函数中释放所有先前分配的资源。
不要使用构造函数或析构函数。
您无权访问任何Isaac Robot Engine功能,例如构造函数中的配置。
您创建的每个自定义代码都需要在Isaac Robot Engine中注册。
这是使用ISAAC_ALICE_REGISTER_CODELET宏在文件末尾完成的。
如果您的codelet在命名空间内,则必须提供完全限定的类型名称,例如:
ISAAC_ALICE_REGISTER_CODELET(foo::bar::MyCodelet)
- 要向codelet添加一些功能,请创建一个名为Ping.cpp的源文件,其中包含以下功能:
#include "Ping.hpp"
void Ping::start() {}
void Ping::tick() {}
void Ping::stop() {}
- Codelet可以以多种不同的方式运行tick,但现在使用定期运行。
- 这可以通过在Ping::start函数中调用tickPeriodically函数来实现。
- 将以下代码添加到Ping.cpp中的start函数
void Ping::start() {
tickPeriodically();
}
- 为了验证事实确实发生了什么,我们在codelet中运行tick时打印一条消息。
- Isaac SDK包含用于记录数据的实用程序功能。
- LOG_INFO可用于在控制台上打印消息。
- 它遵循printf风格的语法。
- 将tick函数添加到Ping.cpp,如下所示:
void Ping::tick() {
LOG_INFO("ping");
}
- 将模块添加到BUILD,如下所示:
isaac_app(
...
)
isaac_cc_module(
name = "ping_components",
srcs = ["Ping.cpp"],
hdrs = ["Ping.hpp"],
)
- Isaac模块定义了一个共享库,它封装了一组codelet,可供不同的应用程序使用。
- 为了在应用程序中使用Ping codelet,我们首先需要在应用程序JSON文件中创建一个新节点:
{
"name": "ping",
"graph": {
"nodes": [
{
"name": "ping",
"components": []
}
],
"edges": []
}
}
- 每个节点可以包含定义其功能的多个组件。
- 通过在components数组中添加一个新部分,将Ping codelet添加到节点:
{
"name": "ping",
"graph": {
"nodes": [
{
"name": "ping",
"components": [
{
"name": "ping",
"type": "Ping"
}
]
}
],
"edges": []
}
}
- 应用程序图通常具有连接不同节点,这些节点确定不同节点之间的消息传递顺序。
- 由于此应用程序没有任何其他节点,因此请将边留空。
- 如果您尝试运行此应用程序,它将会出现紧急情况并显示错误消息无法加载组件“Ping”。
- 发生这种情况是因为必须将应用程序中使用的所有组件添加到模块列表中。
- 您需要在BUILD文件和应用程序JSON文件中执行此操作:
load("//engine/build:isaac.bzl", "isaac_app", "isaac_cc_module")
isaac_app(
name = "ping",
modules = ["//packages/ping:ping_components"]
)
{
"name": "ping",
"modules": [
"ping:ping_components"
],
"graph": {
...
}
}
- 请注意,表达式 ping:ping_components 组件是指我们之前创建的模块//package/ping:ping_components 组件。
- 如果你现在运行应用程序,你会得到一个不同的报错消息:
- 找不到参数'ping/ping/tick_period'或类型错误。
- 出现此消息是因为我们需要在配置部分设置Ping codelet的tick周期。
- 我们将在下一节中完成此操作。
- 配置
大多数代码需要各种参数来自定义行为。
例如,您可能希望为ping计算机的用户提供更改计时周期的选项。
在Isaac框架中,这可以通过配置来实现。
让我们在应用程序JSON文件的配置部分指定tick周期,以便我们最终运行应用程序。
{
"name": "ping",
"modules": [
"ping:ping_components"
],
"graph": {
...
},
"config": {
"ping" : {
"ping" : {
"tick_period" : "1Hz"
}
}
}
}
每个配置参数都由三个元素引用:节点名称,组件名称和参数名称。
在这种情况下,我们在节点ping中设置组件ping的参数tick_period。
现在应用程序将成功运行并将每秒打印一次“ping”。
您应该看到类似于下面的代码段的输出。
您可以按Ctrl + C优雅地停止应用程序。
bob@desktop:~/isaac/packages/ping$ bazel run ping
2019-03-24 17:09:39.726 DEBUG engine/alice/backend/codelet_backend.cpp@61: Starting codelet 'ping/ping' ...
2019-03-24 17:09:39.726 DEBUG engine/alice/backend/codelet_backend.cpp@73: Starting codelet 'ping/ping' DONE
2019-03-24 17:09:39.726 DEBUG engine/alice/backend/codelet_backend.cpp@291: Starting job for codelet 'ping/ping'
2019-03-24 17:09:39.726 INFO packages/ping/Ping.cpp@8: ping
2019-03-24 17:09:40.727 INFO packages/ping/Ping.cpp@8: ping
2019-03-24 17:09:41.726 INFO packages/ping/Ping.cpp@8: ping
- tick_period参数是为我们自动创建的,但我们也可以创建自己的参数来自定义codelet的行为。
- 在codelet中添加一个参数,如下所示:
class Ping : public isaac::alice::Codelet {
public:
void start() override;
void tick() override;
void stop() override;
ISAAC_PARAM(std::string, message, "Hello World!");
};
ISAAC_PARAM有三个参数。
首先是参数的类型。
通常,这是double,int,bool或std :: string。
第二个参数是我们参数的名称。
该名称用于访问或指定参数。
第三个参数是用于此参数的默认值。
如果没有给出默认值且未通过配置文件指定参数,则程序在访问参数时置位。
ISAAC_PARAM宏创建一个名为get_message的访问器和一些代码,以便将参数与系统的其余部分正确连接。
我们现在可以在tick函数中使用参数而不是硬编码值:
void tick() {
LOG_INFO(get_message().c_str());
}
- 下一步是添加节点的配置。
- config参数使用节点名称,组件名称和参数名称来指定所需的值。
{
"name": "ping",
"modules": [
"ping:ping_components"
],
"graph": {
...
},
"config": {
"ping" : {
"ping" : {
"message": "My own hello world!",
"tick_period" : "1Hz"
}
}
}
}
- 您现在有一个可以定期打印自定义消息的应用程序。
- 使用以下命令运行该应用程序:
bob@desktop:~/isaac/packages/ping$ bazel run ping
- 正如所料,codelet在命令行上定期打印消息。
- 发送信息
- 自定义codelet Ping很顺利运行 。
- 为了让其他节点对ping作出反应,Ping codelet必须发送其他codelet可以接收的消息。
- 发布消息很容易。
- 使用ISAAC_PROTO_TX宏指定codelet正在发布消息。
- 将其添加到Ping.hpp,如下所示:
#pragma once
#include "engine/alice/alice.hpp"
#include "messages/messages.hpp"
class Ping : public isaac::alice::Codelet {
public:
...
ISAAC_PARAM(std::string, message, "Hello World!");
ISAAC_PROTO_TX(PingProto, ping);
};
ISAAC_ALICE_REGISTER_CODELET(Ping);
ISAAC_PROTO_TX宏有两个参数。
第一个指定要发布的消息。
在这里,使用PingProto消息作为Isaac消息API的一部分。
通过包含相应的标头来访问PingProto。
第二个参数指定我们要在其下发布消息的通道的名称。
接下来,更改tick功能以发布消息,而不是打印到控制台。
Isaac SDK目前支持capnproto消息。
Protos是一种独立于平台和语言的表示和序列化数据的方式。
通过调用ISAAC_PROTO_TX宏创建的访问器上的initProto函数来启动创建消息。
此函数返回capnproto构建器对象,该对象可用于将数据直接写入proto。
ProtoPing消息有一个名为string类型的消息的字段,因此在这个实例中我们可以使用setMessage函数将一些文本写入proto。 - 填充proto后,我们可以通过调用发布函数来发送消息。
这会立即将消息发送给任何连接的接收器。
将Ping.cpp中的tick()函数更改为以下内容:
...
void Ping::tick() {
// create and publish a ping message
auto proto = tx_ping().initProto();
proto.setMessage(get_message());
tx_ping().publish();
}
...
- 最后,升级节点(在JSON文件中)以支持消息传递。
- 默认情况下,Isaac SDK中的节点是轻量级对象,只需要最少的强制组件设置。
- 不一定,应用程序中的每个节点都会发布或接收消息。
- 要在节点上启用消息传递,我们需要添加一个名为MessageLedger的组件。
- 此组件处理传入和传出消息,并将它们中继到其他节点中的MessageLedger组件。
{
"name": "ping",
"graph": {
"nodes": [
{
"name": "ping",
"components": [
{
"name": "message_ledger",
"type": "isaac::alice::MessageLedger"
},
{
"name": "ping",
"type": "Ping"
}
]
}
],
"edges": []
},
"config": {
...
}
- 构建并运行应用程序。
- 似乎没有任何反应,因为现在没有任何东西与您的频道相关联。
- 在发布消息时,没有人接收消息并对其做出反应。
- 您将在下一节中解决这个问题。
- 接受信息
- 您需要一个可以接收ping消息并以某种方式对其做出反应的节点。
- 为此,让我们创建一个Pong codelet,它由Ping发送的消息触发。
- 使用以下内容创建一个新文件Pong.hpp:
#pragma once
#include "engine/alice/alice.hpp"
#include "messages/messages.hpp"
class Pong : public isaac::alice::Codelet {
public:
void start() override;
void tick() override;
// An incoming message channel on which we receive pings.
ISAAC_PROTO_RX(PingProto, trigger);
// Specifies how many times we print 'PONG' when we are triggered
ISAAC_PARAM(int, count, 3);
};
ISAAC_ALICE_REGISTER_CODELET(Pong);
- 需要将Pong codelet添加到ping_components模块才能进行编译。
- 将其添加到BUILD文件中,如下所示:
isaac_cc_module(
name = "ping_components",
srcs = [
"Ping.cpp",
"Pong.cpp"
],
hdrs = [
"Ping.hpp",
"Pong.hpp"
],
)
- 在应用程序JSON中,创建第二个节点并将新的Pong codelet附加到它。
- 通过边连接Ping和Pong节点:
{
"name": "ping",
"modules": [
"ping:ping_components"
],
"graph": {
"nodes": [
{
"name": "ping",
"components": [
{
"name": "message_ledger",
"type": "isaac::alice::MessageLedger"
},
{
"name": "ping",
"type": "Ping"
}
]
},
{
"name": "pong",
"components": [
{
"name": "message_ledger",
"type": "isaac::alice::MessageLedger"
},
{
"name": "pong",
"type": "Pong"
}
]
}
],
"edges": [
{
"source": "ping/ping/ping",
"target": "pong/pong/trigger"
}
]
},
"config": {
"ping" : {
"ping" : {
"message": "My own hello world!",
"tick_period" : "1Hz"
}
}
}
}
edges将接收RX信道连接到发送TX信道。
发送信道可以将数据发送到多个接收器。
接收通道也可以从多个发射器接收数据,但是这有警告并且不鼓励。
与参数类似,通道由三个元素引用:节点名称,组件名称和通道名称。
可以通过将edges添加到应用程序JSON文件中的“edges”部分来创建边。
source是发送通道的全名,target是接收通道的全名。
最后剩下的任务是设置Pong codelet在收到ping时做某事。
创建一个新文件Pong.cpp。
在start中调用tickOnMessage函数,指示codelet每次在该通道上收到新消息时进行tick。
在tick中,我们添加了打印出“PONG!”的功能,其次数由Pong的头文件中的“count”参数定义:
#include "Pong.hpp"
#include <cstdio>
void Pong::start() {
tickOnMessage(rx_trigger());
}
void Pong::tick() {
// Parse the message we received
auto proto = rx_trigger().getProto();
const std::string message = proto.getMessage();
// Print the desired number of 'PONG!' to the console
const int num_beeps = get_count();
std::printf("%s:", message.c_str());
for (int i = 0; i < num_beeps; i++) {
std::printf(" PONG!");
}
if (num_beeps > 0) {
std::printf("\n");
}
}
通过使用tickOnMessage而不是tickPeriodically,我们指示codelet仅在传入数据通道上接收到新消息时进行tick处理,在本例中为触发器。
tick功能现在只在您收到新消息时执行。
这是由isaac机器人引擎保证的。
运行该应用程序。
每次Pong codelet从Ping codelet接收ping消息时,您应该看到如何生成“pong”。
通过更改配置文件中的参数,您可以更改创建ping的时间间隔,更改与每次ping一起发送的消息,并在收到ping时或多或少地打印pong。
这只是一个非常简单的应用程序的快速入门。
实际应用程序由数十个节点组成,每个节点具有多个组件,大多数具有一个或多个小Codelet。
Codelet接收多种类型的消息,调用专用库来解决硬计算问题,并再次发布其结果以供其他节点使用。
获取最新文章: 扫一扫右上角的二维码加入“创客智造”公众号