用C ++开发Codelet(Codelet)
https://docs.nvidia.com/isaac/isaac/doc/tutorials/ping.html
本教程的目标是用C ++开发一段小代码(Codelet,简称小码),该小码(Codelet)实际上是可以“ ping”的机器人程序。对于本教程,不需要外部依赖项或特殊硬件。
首先通过使用以下命令在isaac / packages目录中创建一个文件夹来创建新软件包:
bob@desktop:~/isaac/packages/$ mkdir ping
对于本教程的其余部分,每当要求您创建一个新文件时,请将其直接放置到此文件夹中。更复杂的软件包将包含子文件夹,但对于本入门教程, 越简单越好。
Isaac应用程序基于JSON文件创建。JSON文件描述了应用程序的依赖关系(dependencies),节点图(node graph)和消息流(message flow),并包含自定义配置。创建一个名为ping.app.json
的JSON文件,使用以下代码指定其名称:
{
"name": "ping"
}
接下来,创建一个bazel构建文件来编译和运行该程序。Bazel为大型项目提供了良好的依赖关系管理以及出色的编译速度,而且bazel build文件非常易于编写。使用name 项创建一个新文件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
来停止正在运行的程序。此操作会正常关闭应用程序。您会注意到什么都没发生,因为我们还没有一个应用程序图(graph)。接下来,我们将为应用程序创建一些节点。
创建一个节点(Node)
Isaac应用程序由许多并行运行的节点组成。他们可以使用Isaac机器人引擎提供的各种机制互相发送消息或进行交互。节点轻量,不需要自己的进程,甚至不需要自己的线程。
要自定义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
。启动节点时,将首先调用所有附加小码的启动(start
)函数。例如,start
是分配资源的好地方。您可以将小码tick
配置为定期触发或每次收到新消息时触发。程序大部分功能由tick
函数执行。
最后,当节点停止时,将调用stop
函数。您应该在stop
函数中释放所有先前分配的资源。不要使用构造函数或析构函数。您无权访问任何Isaac Robot Engine功能,例如构造函数中的配置。
每个自定义小码都需要向Isaac Robot Engine注册。这是在文件末尾使用ISAAC_ALICE_REGISTER_CODELET
宏完成的。为确保您的小码位于名称空间内,必须提供完全限定的类型名称,例如 ISAAC_ALICE_REGISTER_CODELET(foo::bar::MyCodelet);
。
要向小码添加一些功能,请创建一个名为Ping.cpp
的源文件,其中包含以下功能:
#include "Ping.hpp"
void Ping::start() {}
void Ping::tick() {}
void Ping::stop() {}
小码可以以多种方式进行滴答(执行),但是现在使用定期滴答(periodic ticking)。这可以通过在小码Ping::start
函数中调用tickPeriodically
函数来实现。将以下代码添加到Ping.cpp
的start
函数中
void Ping::start() {
tickPeriodically();
}
为了验证是否执行正确,我们在小码滴答声中打印一条消息。Isaac SDK包含用于记录数据的函数。LOG_INFO可用于在控制台上打印消息。它遵循printf-style语法。将该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模块定义了一个共享库,该库封装了一组小码,可以由不同的应用程序使用。
为了在应用程序中使用Ping小码,我们首先需要在应用程序JSON文件中创建一个新节点:
{
"name": "ping",
"graph": {
"nodes": [
{
"name": "ping",
"components": []
}
],
"edges": []
}
}
每个节点可以包含定义其功能的多个组件。通过在components数组中添加新的部分,将Ping小码添加到节点中:
{
"name": "ping",
"graph": {
"nodes": [
{
"name": "ping",
"components": [
{
"name": "ping",
"type": "Ping"
}
]
}
],
"edges": []
}
}
应用程序图(graph)通常具有连接不同节点的边缘(edges),这些边确定了不同节点之间的消息传递顺序。因为此应用程序没有任何其他节点,所以将边缘(edges)留为空。
如果您尝试运行此应用程序,则会出现警告并显示错误消息Could not load component ‘Ping’.。发生这种情况是因为程序要求必须将应用程序中使用的所有组件添加到模块(modules)列表中。您需要同时在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
。
如果您现在运行该应用程序,则会收到另一个警告消息: Parameter ‘ping/ping/tick_period’ not found or wrong type。出现此消息是因为我们需要在配置部分中设置Ping小码的滴答周期。我们将在下一部分中进行此操作。
配置(Configuration)
大多数代码需要各种参数来自定义行为。例如,您可能希望为我们的ping用户提供更改滴答周期的选项。在Isaac框架中,这可以通过配置来实现。
让我们在应用程序JSON文件的配置部分中指定周期,以便我们最终可以运行该应用程序。
{
"name": "ping",
"modules": [
"ping:ping_components"
],
"graph": {
...
},
"config": {
"ping" : {
"ping" : {
"tick_period" : "1Hz"
}
}
}
}
每个配置参数都由三个元素引用:节点名称(node name),组件名称(component name)和参数名称(parameter name)。在这种情况下,我们在节点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
参数是引擎自动为我们创建,但我们也可以创建自己的参数来定制小码的行为。如下所示,将参数添加到您的代码中:
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
访问(注:参数为message,创建函数get_message,如果参数为value1,则会对应创建get_value1)和其他参数以保证与系统的其余部分正确结合。
我们现在可以在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
和预期一样,小码会在命令行上定频打印消息。
发送消息(Sending Messages)
定制小码Ping的很愉快。为了使其他节点对ping进行响应,Ping小码必须发送给其他小码可以接收的消息。
发布消息很容易。使用ISAAC_PROTO_TX
宏可以指定小码正在发布消息。如下所示将其添加到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当前支持Cap'n'proto消息。Protos是一种用来表示和序列化数据,并且独立于平台和语言的方式。通过调用ISAAC_PROTO_TX
宏创建的访问器上的initProto
函数来创建消息。此函数返回cap'n'proto构建器对象,该对象可用于直接将数据写入原型。
该ProtoPing
消息具有一个称为message
字符串类型的字段,这里,我们可以使用setMessage
函数向原型写入一些文本。写入后,我们就可以通过调用publish函数发送消息。这将立即将消息发送到任何已连接的接收器。将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": {
...
}
生成并运行,似乎什么也没发生,因为目前您的发布通道没有连接任何接收器。发布消息时,没有人可以接收消息并对消息做出响应。您将在下一节对其进行修复。
接收消息(Receiving Messages)
您需要一个可以接收ping消息并以某种方式对其做出反应的节点。为此,让我们创建一个小码Pong
,该小码由发送的消息触发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
需要被添加到ping_components模块,才能编译。如下所示将其添加到BUILD文件中:
isaac_cc_module(
name = "ping_components",
srcs = [
"Ping.cpp",
"Pong.cpp"
],
hdrs = [
"Ping.hpp",
"Pong.hpp"
],
)
在应用程序JSON中,创建第二个节点,并将新的小码Pong附加到该节点。通过边缘(edges)连接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通道。发送TX通道可以将数据发送到多个接收器。接收RX通道也可以从多个发射器接收数据,但要注意,不建议这样做。与参数相似,通道由三个元素引用:节点名称(node name),组件名称(component name)和通道名称(channel name)。可以通过将边缘添加到应用程序JSON文件的“ edges”部分中来创建边缘。这里source是发送通道的全名,而target是接收通道的全名。
剩下的最后一项任务是设置Pong小码,使其在收到ping时执行某些操作。创建一个新文件Pong.cpp。在函数start
中调用tickOnMessage
以指示小码每次在该频道上收到新消息时打印输出。tick
中,我们添加了打印“ PONG!”的功能,其次数为Pong头文件中“ count”参数所定义的次数:
#include "Pong.hpp"
#include
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
,我们指示小码仅在传入数据通道上接收到新消息时才执行,也就是触发(trigger)。现在,滴答功能仅在您收到新消息时才执行。艾萨克机器人引擎(Isaac Robot Engine)确保其正确运行。
运行应用程序。您应该看到每次Pong小码从Ping小码接收到ping消息时如何生成“ pong”。通过更改配置文件中的参数,您可以更改创建ping的间隔,更改与每个ping一起发送的消息,并在收到ping时打印pong的次数。
通过网络发送消息
如果Ping
和Pong
在不同的设备上运行,则需要网络连接。如下所示,在 TcpPublisher
和 TcpSubscriber
建立网络连接:
{
"name": "ping",
...
"graph": {
"nodes": [
...
{
"name": "pub",
"components": [
{
"name": "message_ledger",
"type": "isaac::alice::MessageLedger"
},
{
"name": "tcp_publisher",
"type": "isaac::alice::TcpPublisher"
}
]
}
],
"edges": [
{
"source": "ping/ping/ping",
"target": "pub/tcp_publisher/tunnel"
}
]
},
"config": {
...
"pub": {
"tcp_publisher": {
"port": 5005
}
}
}
}
该port
参数指定连接的网络端口。确保它在设备上可用。另一方面,TcpSubscriber
在JSON文件中进行设置时可以传递消息,如下所示:
{
"name": "pong",
...
"graph": {
"nodes": [
...
{
"name": "sub",
"components": [
{
"name": "message_ledger",
"type": "isaac::alice::MessageLedger"
},
{
"name": "tcp_receiver",
"type": "isaac::alice::TcpSubscriber"
}
]
}
],
"edges": [
{
"source": "sub/tcp_receiver/tunnel",
"target": "pong/pong/trigger"
}
]
},
"config": {
...
"sub": {
"tcp_receiver": {
"port": 5005,
"reconnect_interval": 0.5,
"host": "127.0.0.1"
}
}
}
}
该host
参数指定要监听的IP地址。确保host
和port
指定Ping
运行设备的端口和IP地址。在单独的设备上运行这些应用程序以查看通过网络传递的消息。
这只是一个非常简单的应用程序的快速入门。实际应用程序由数十个节点组成,每个节点具有多个组件,并且大多数具有一个或多个小码。小码接收多种类型的消息,调用专门的库来解决困难的计算问题,然后发布其结果以供其他节点使用。