0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C++でFoxgloveに入門する(ROSなし)

Posted at

はじめに

Foxgloveがビジュアライザとして面白そうなので、入門して簡単なビジュアライズに挑戦してみます。ROS2を使わずC++のみで実装した場合について説明します。

Foxgloveとは

Foxgloveは、ロボットなどのフィジカルAIのデータを視覚化・記録・デバッグするためのオープンソースのツールです。もっと簡単にいえば、ビジュアライザとロガーが合わさったようなツールです。

Foxgloveでは3D空間・グラフ・動画などを表示するパネルを組み合わせることで自由度の高い視覚化ができます。

Foxgloveのレイアウト(GUI)例
videoframe_1059.png

videoframe_8000.png

なお、チーム内ユーザが3人以下などの制限はありますが、無料で利用することができます(アカデミック用途であれば無料かつ大半の制限なしで使えます)。

やること

  • Foxgloveのパネル・レイアウト(各パネルの配置)作成
  • データの表示・グラフ化

やらないこと

  • ログファイルへの記録・読み出し
  • ROS・ROS2の使用
  • 3Dモデルなどの複雑なビジュアライズ

環境

  • CPU i5-1240P(アーキテクチャ x86_64)
  • WSL2 (Ubuntu24.04)
  • gcc version 13.3.0
  • cmake version 3.28.3
  • foxglove-sdk v0.13.0

インストール

  • C++のコンパイラ
  • cmake
  • Foxglove:Web版デスクトップ版がありますが、今回の使用用途ではどちらでも大丈夫です。リンク先からログインorインストールしてください。今回はWeb版で説明します
  • foxglove-sdk:後述します

コンパイラとcmakeのインストール手順は調べれば出るので省略します。

レイアウトを作成する

Foxgloveにログインするとダッシュボードが開きます。まずは可視化用のデータを受け取るために新しく接続を開きましょう。ダッシュボード左側から接続を開くを選択します。

image.png

WebSocket URLの入力を求められますが、デフォルトのws://localhost:8765でOK。
image.png

接続を開くと、おそらくパネルの選択を求められると思います。グラフ用にプロットパネルを選択。

image.png

現在のレイアウトがこんな感じ。
image.png

せっかくなのでパネルを追加してみます。画面右上の+アイコンを押してパネルを追加。選択肢がいくつか出てきますが、今回は生データのメッセージパネルを選択。

image.png

いまのレイアウトはこんな感じ。
image.png

各パネルをドラッグすると伸縮・配置変更が可能です。
image.png

また、各パネルのケバブメニュー(縦3点ドット)をクリックするとパネルの変更や削除などができます。

image.png

データを送る・確認する

C++のプログラムを作成して、Foxgloveにデータを送り、Foxgloveで確認します。先に完成品を見せるとこんな感じ。

image.png

cmakeの準備

まずはcmakeの準備から。CMakeLists.txtを作成し、以下の内容をコピペします。

CMakeLists.txt
cmake_minimum_required(VERSION 3.20)

# TODO: replace "my_program" throughout with the name of your project
project(my_program LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)

# Fetch Foxglove SDK
include(FetchContent)
FetchContent_Declare(
    foxglove
    DOWNLOAD_EXTRACT_TIMESTAMP TRUE
    # TODO: Copy the URL and SHA for the appropriate .zip file from here:
    # https://github.com/foxglove/foxglove-sdk/releases?q=sdk%2F&expanded=true
    URL <download URL from the GitHub release>
    URL_HASH SHA256=<sha from the GitHub release>
)
FetchContent_MakeAvailable(foxglove)

# Assuming the project consists of a single source file, main.cpp
add_executable(my_program main.cpp)

# Add include directory for Foxglove SDK
target_include_directories(my_program PRIVATE ${foxglove_SOURCE_DIR}/include)
target_include_directories(my_program PRIVATE ${foxglove_SOURCE_DIR}/include/foxglove)

# Find all Foxglove SDK source files
file(GLOB FOXGLOVE_SOURCES CONFIGURE_DEPENDS
    "${foxglove_SOURCE_DIR}/src/*.cpp"
    "${foxglove_SOURCE_DIR}/src/server/*.cpp"
)

# Add Foxglove SDK source files
target_sources(my_program PRIVATE ${FOXGLOVE_SOURCES})

# Link against libfoxglove.a
target_link_libraries(my_program PRIVATE ${foxglove_SOURCE_DIR}/lib/libfoxglove.a)

次に、foxglove-sdkをインストールするためにリリースページへ飛び、最新バージョンのAssetsから自分の環境にあったものを見つけます。自分の場合はCPUアーキテクチャがx86_64でOSがLinux(WSL2)なのでfoxglove-v0.13.0-cpp-x86_64-unknown-linux-gnu.zipになります。

image.png

見つけたzipファイルのダウンロードリンクと、その横のsha256の値をそれぞれコピーしてCMakeLists.txtに貼り付けます。

CKakeLists.txt
cmake_minimum_required(VERSION 3.20)

# TODO: replace "my_program" throughout with the name of your project
project(my_program LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)

# Fetch Foxglove SDK
include(FetchContent)
FetchContent_Declare(
    foxglove
    DOWNLOAD_EXTRACT_TIMESTAMP TRUE
    # TODO: Copy the URL and SHA for the appropriate .zip file from here:
    # https://github.com/foxglove/foxglove-sdk/releases?q=sdk%2F&expanded=true
-    URL <download URL from the GitHub release>
-    URL_HASH SHA256=<sha from the GitHub release>
+    URL https://github.com/foxglove/foxglove-sdk/releases/download/sdk%2Fv0.13.0/foxglove-v0.13.0-cpp-x86_64-unknown-linux-gnu.zip
+    URL_HASH SHA256=609761f7e76c464835d82b8d2abd02d9b4c975a77cbc12515ebdd0567e87023e
)
FetchContent_MakeAvailable(foxglove)

# Assuming the project consists of a single source file, main.cpp
add_executable(my_program main.cpp)

# Add include directory for Foxglove SDK
target_include_directories(my_program PRIVATE ${foxglove_SOURCE_DIR}/include)
target_include_directories(my_program PRIVATE ${foxglove_SOURCE_DIR}/include/foxglove)

# Find all Foxglove SDK source files
file(GLOB FOXGLOVE_SOURCES CONFIGURE_DEPENDS
    "${foxglove_SOURCE_DIR}/src/*.cpp"
    "${foxglove_SOURCE_DIR}/src/server/*.cpp"
)

# Add Foxglove SDK source files
target_sources(my_program PRIVATE ${FOXGLOVE_SOURCES})

# Link against libfoxglove.a
target_link_libraries(my_program PRIVATE ${foxglove_SOURCE_DIR}/lib/libfoxglove.a)

main.cppの準備

Foxgloveにデータを送信するプログラムであるmain.cppを作成します。ほとんどドキュメントの内容そのままですが、自分の環境だと#include <atomic>がないと動かなかったので足しています。

内容は、プログラム実行時からの経過時間をjson形式でFoxgloveに送信するものです。なお、チャンネルトピックなどの用語が出てきますが、この記事の最後でまとめて解説します。

main.cpp
#include <foxglove/foxglove.hpp>
#include <foxglove/server.hpp>

#include <chrono>
#include <csignal>
#include <iostream>
#include <thread>
#include <atomic> //ドキュメントにはないので追加

using namespace std::chrono_literals;

int main(int argc, const char *argv[]) {
  foxglove::WebSocketServerOptions options;
  auto serverResult = foxglove::WebSocketServer::create(std::move(options));
  if (!serverResult.has_value()) {
    std::cerr << foxglove::strerror(serverResult.error()) << '\n';
    return 1;
  }

  auto server = std::move(serverResult.value());

  // トピック名を/helloとしてjson形式で書き込むチャンネルを作成
  auto channel = foxglove::RawChannel::create("/hello", "json").value();
  auto start = std::chrono::steady_clock::now();

  // Log until interrupted
  static std::function<void()> sigint_handler;
  std::atomic_bool done = false;
  sigint_handler = [&] { done = true; };
  std::signal(SIGINT, [](int) {
    if (sigint_handler) {
      sigint_handler();
    }
  });

  while (!done) {
    auto dur = std::chrono::steady_clock::now() - start;
    float elapsed_seconds = std::chrono::duration<float>(dur).count();
    // "elapsed"をキーとするjson形式のデータを作成。値は経過時間。
    std::string msg = "{\"elapsed\": " + std::to_string(elapsed_seconds) + "}";
    // msgをfoxgloveに送信する。
    channel.log(reinterpret_cast<const std::byte *>(msg.data()), msg.size());

    std::this_thread::sleep_for(33ms);
  }

  return 0;
}

コンパイル

cmakeとmakeを実行してバイナリファイルを作成します。初回はfoxglove-sdkのインストールも行われると思います。

sin471@LAPTOP-SISACVID:~/foxglove$ ls
CMakeLists.txt  main.cpp
sin471@LAPTOP-SISACVID:~/foxglove$ mkdir build
sin471@LAPTOP-SISACVID:~/foxglove$ cd build/
sin471@LAPTOP-SISACVID:~/foxglove/build$ cmake ..
-- The CXX compiler identification is GNU 13.3.0
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done (7.1s)
-- Generating done (0.0s)
-- Build files have been written to: /home/sin471/foxglove/build
sin471@LAPTOP-SISACVID:~/foxglove/build$ make -j8
[  7%] Building CXX object CMakeFiles/my_program.dir/main.cpp.o
[ 23%] Building CXX object CMakeFiles/my_program.dir/_deps/foxglove-src/src/error.cpp.o
[ 30%] Building CXX object CMakeFiles/my_program.dir/_deps/foxglove-src/src/context.cpp.o
[ 30%] Building CXX object CMakeFiles/my_program.dir/_deps/foxglove-src/src/channel.cpp.o
[ 38%] Building CXX object CMakeFiles/my_program.dir/_deps/foxglove-src/src/foxglove.cpp.o
[ 46%] Building CXX object CMakeFiles/my_program.dir/_deps/foxglove-src/src/mcap.cpp.o
[ 53%] Building CXX object CMakeFiles/my_program.dir/_deps/foxglove-src/src/schemas.cpp.o
[ 61%] Building CXX object CMakeFiles/my_program.dir/_deps/foxglove-src/src/server.cpp.o
[ 69%] Building CXX object CMakeFiles/my_program.dir/_deps/foxglove-src/src/server/connection_graph.cpp.o
[ 76%] Building CXX object CMakeFiles/my_program.dir/_deps/foxglove-src/src/server/fetch_asset.cpp.o
[ 84%] Building CXX object CMakeFiles/my_program.dir/_deps/foxglove-src/src/server/parameter.cpp.o
[ 92%] Building CXX object CMakeFiles/my_program.dir/_deps/foxglove-src/src/server/service.cpp.o
[100%] Linking CXX executable my_program
[100%] Built target my_program

バイナリの実行

コンパイルが終わるとbuildディレクトリ内にmy_programというバイナリができているので実行します。

sin471@LAPTOP-SISACVID:~/foxglove/build$ ./my_program 

Foxgloveで確認

バイナリを実行したままもう一度Foxgloveを開き、ページをリロードします。そして生データのメッセージパネルのEnter a message pathの部分に/helloと入れると/helloトピックの内容、すなわち経過時間が表示されます。

image.png

また、プロットパネルのシリーズを追加するにはクリックをクリックし、出てきたサイドパネルのシリーズY-value path/hello.elapsedと入力すると、経過時間がグラフで確認できます。

image.png

JSONを使わずに送る

先程は送信データをJSON形式にしてから送信していましたが、JSONを使わずに送信することをもできます。今回はLogクラスを使って2種類のメッセージをログとして送ってみます。

main.cpp
#include <foxglove/foxglove.hpp>
#include <foxglove/server.hpp>

#include <chrono>
#include <csignal>
#include <iostream>
#include <thread>
#include <atomic> //ドキュメントにはないので追加

using namespace std::chrono_literals;

int main(int argc, const char *argv[]) {
  foxglove::WebSocketServerOptions options;
  auto serverResult = foxglove::WebSocketServer::create(std::move(options));
  if (!serverResult.has_value()) {
    std::cerr << foxglove::strerror(serverResult.error()) << '\n';
    return 1;
  }

  auto server = std::move(serverResult.value());
  // トピック名を/infoとしてLog構造体を書き込むチャンネルを作成
  auto channel = foxglove::schemas::LogChannel::create("/info").value();
  // トピック名を/warnとしてLog構造体を書き込むチャンネルを作成
  auto channel2 = foxglove::schemas::LogChannel::create("/warn").value();
  auto start = std::chrono::steady_clock::now();

  // Log until interrupted
  static std::function<void()> sigint_handler;
  std::atomic_bool done = false;
  sigint_handler = [&] { done = true; };
  std::signal(SIGINT, [](int) {
    if (sigint_handler) {
      sigint_handler();
    }
  });

  while (!done) {
    // ログレベルとメッセージを指定
    foxglove::schemas::Log log;
    foxglove::schemas::Log log2;
    log.level = foxglove::schemas::Log::LogLevel::INFO;
    log.message = "Hello, world!";
    // 構造体を直接渡して書き込む
    channel.log(log);

    log2.level = foxglove::schemas::Log::LogLevel::WARNING;
    log2.message = "Warning: Something might be wrong.";
    channel2.log(log2);

    std::this_thread::sleep_for(33ms);
  }
  
  return 0;
}

先ほどはfoxglove::schemas::RawChannel::createだったのがfoxglove::schemas::LogChannel::createに変化していることに注意してください。このように、送るデータ形式にあったチャンネルをcreate()することでクラスのインスタンスを直接送信することができます。今回の場合はLogクラスを送信するのでLogChannelを使用してチャンネルを作成しました。

なお、ほかにも2次元座標を表すPoint2DPoint2DChannelなど、さまざまなクラスとチャンネルが用意されているので用途に合わせて適切なものを選択しましょう。

さて、先ほどと同様にコンパイル・実行ののちFoxgloveで確認します。
生データのメッセージパネルを2つにしてそれぞれ/info,/warnトピックを指定するとこんな感じ。メッセージやログレベルなどの内容が表示されていることがわかります。

image.png

シンク・チャンネル・トピック

最後に、説明していなかったシンクチャンネルトピックについて説明します。まだ理解しきれてないので少し曖昧です。

まず、シンクはデータの書き込み先です。今回はFoxgloveのWebソケットサーバにデータを送信していたのでシンクはWebソケットサーバです。ところで、Foxgloveではデータをmcapファイルとして保存することも可能であり、その場合のシンクはmcapファイルになります。

つぎに、チャンネルはデータを送信するためのものです。各データは最終的にチャンネルの機能を通じてシンクへと書き込まれます。今回見たようにチャンネルには複数の種類があり、JSONなどでデータを送信できるfoxglove::RawChannelやログ形式のデータ(foxglove::schemas::Log)を送信できるfoxglove::schemas::LogChannelなどがあります。

最後に、トピックはチャンネルのインスタンスを区別するための名前です。各トピック名はチャンネルのインスタンスごとに一意で、かつ/ではじまる必要があります。今回の記事でいえば/hello,/info,/warnがトピック名になります。Foxgloveではトピック名を指定することで可視化したいデータを指定します。

今回の例を使ってチャンネル・トピック・シンクの関係を図にするとこんな感じ。

channel-topic.drawio.png

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?