Waveshare JetBot AI Kit — ROS 2 (Foxy) セットアップ記録
対象機材: Waveshare JetBot AI Kit(0.91インチOLED搭載、TB6612FNGモータードライバ、I2C接続の標準的なJetBot構成)
ベース環境: Jetson Nano / JetPack 4.6.x(L4T R32.7.1)/ Ubuntu 18.04 (bionic)
目標: dusty-nv/jetbot_ros を使い、ROS 2 Foxy環境でモーターとOLEDを動かす
OSイメージの書き込み
1.ベースOS(JetPack 4.6)の書き込みROS 2のDockerコンテナを動かすための土台となる
公式OSを準備します。パソコンで NVIDIA公式のJetPack 4.6.1 イメージ (Ubuntu 18.04)
https://developer.nvidia.com/embedded/jetpack-sdk-461 をダウンロードします。
2.BalenaEtcher などのソフトを使い、64GB以上のSDカードに書き込みます。
3.書き終わったらSDカードをJetson Nanoに挿入し、HDMIモニター、キーボード、
マウスを繋いで電源を入れます。
4.画面の指示に従ってUbuntuの初期設定(言語、ユーザー名、パスワードの設定)を完了させます。
画面右上のアイコンからWi-Fiに接続します。
ROS環境作成の全体の流れ(サマリ)
-
git cloneのURL指定ミスを修正 - 既製Dockerイメージが存在しない問題 → ローカルビルドに切り替え
- ROS 2公式APT署名鍵の期限切れ問題を解決
- Jetson Nanoのメモリ不足によるビルド失敗を解決
-
ros_deep_learningとTensorRTのバージョン不整合を回避 - コンテナ起動、OLED表示成功
- カメラ依存ノードによる連鎖クラッシュを解消(モーター専用launchファイル作成)
- GUI(lxterminal)依存のteleop起動方法を回避し、キーボード操作でモーター動作を確認
- 作業内容の保存(Dockerコミット、git記録)
- CSIカメラ対応(Bayer形式の確認 →
nvarguscamerasrcを使った独自ノードで解決)
修正箇所まとめ(Dockerfile diff)
~/jetbot_ros/Dockerfile に対して行った変更点を、修正前後の対比でまとめる。
修正①:FROM 直後にROS 2リポジトリの鍵検証無効化を追加
FROM ${BASE_IMAGE}
+RUN echo "deb [trusted=yes arch=arm64] http://packages.ros.org/ros2/ubuntu bionic main" > /etc/apt/sources.list.d/ros2.list
SHELL ["/bin/bash", "-c"]
修正②:並列ビルド数を1に制限(メモリ不足対策)
-ARG MAKEFLAGS=-j$(nproc)
+ARG MAKEFLAGS=-j1
修正③:ros_deep_learning パッケージのビルドをコメントアウト(TensorRT不整合の回避)
#
# ros_deep_learning package
#
-RUN source ${ROS_ENVIRONMENT} && \
- cd ${WORKSPACE_ROOT}/src && \
- git clone https://github.com/dusty-nv/ros_deep_learning && \
- cd ../ && \
- colcon build --symlink-install --event-handlers console_direct+
+#RUN source ${ROS_ENVIRONMENT} && \
+# cd ${WORKSPACE_ROOT}/src && \
+# git clone https://github.com/dusty-nv/ros_deep_learning && \
+# cd ../ && \
+# colcon build --symlink-install --event-handlers console_direct+
追加ファイル:launch/jetbot_motors_only.launch.py(新規)
ros_deep_learning のノード(video_source / video_output)に依存しない、モーター+OLED専用のlaunchファイルを新規追加(内容は8章参照)。
追加ファイル:csi_camera_node.py(新規、Dockerfileの外・コンテナ内に直接作成)
CSIカメラが生Bayer形式(RG10)でしか出力されず、v4l2_cameraでは使えないと判明したため、nvarguscamerasrc(Argus ISP経由)とOpenCV・cv_bridgeを使い、/jetbot/camera/image_rawトピックへ配信する独自のPythonノードを追加(内容は10章参照)。colconの再ビルドは不要で、cv_bridgeはベースイメージに既存だった。
ホスト側でのシステム変更(Dockerfileには含まれない)
| 変更内容 | コマンド |
|---|---|
| スワップ4GB追加 | sudo fallocate -l 4G /swapfile && sudo chmod 600 /swapfile && sudo mkswap /swapfile && sudo swapon /swapfile |
| Dockerイメージの確定保存 | sudo docker commit jetbot_ros jetbot_ros:foxy-r32.7.1-stable |
正しい手順(最初からやり直す場合のクリーン版)
今回は問題が見つかるたびに1つずつ後から修正したが、同じ機材(Waveshare JetBot AI Kit + Jetson Nano + JetPack 4.6.x)でもう一度セットアップし直す場合は、以下の順番で最初からまとめて対処すればつまずかずに進められる。
手順1:スワップ領域を先に確保しておく
sudo fallocate -l 4G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
手順2:リポジトリを正しいURLでクローン
git clone https://github.com/dusty-nv/jetbot_ros
cd jetbot_ros
手順3:Dockerfileを事前に3箇所まとめて修正
# ① ROS2リポジトリの署名鍵検証を無効化(FROM直後に追加)
sed -i '/^FROM ${BASE_IMAGE}/a\
RUN echo "deb [trusted=yes arch=arm64] http://packages.ros.org/ros2/ubuntu bionic main" > /etc/apt/sources.list.d/ros2.list' Dockerfile
# ② 並列ビルド数を1に制限(Jetson Nanoのメモリ不足対策)
sed -i 's|ARG MAKEFLAGS=-j\$(nproc)|ARG MAKEFLAGS=-j1|' Dockerfile
# ③ ros_deep_learning(TensorRT不整合が起きるパッケージ)のビルドを除外
# ※行番号は環境によりずれる可能性があるため、事前に grep -n "ros_deep_learning" Dockerfile で確認してから対象行を指定する
sed -i '/git clone https:\/\/github.com\/dusty-nv\/ros_deep_learning/,+3s/^/#/' Dockerfile
sed -i '/# ros_deep_learning package/,+1!b; /RUN source \${ROS_ENVIRONMENT}/s/^/#/' Dockerfile
確認:
grep -n -A 1 "FROM \${BASE_IMAGE}" Dockerfile
grep -n "MAKEFLAGS" Dockerfile
grep -n -B 1 -A 5 "ros_deep_learning package" Dockerfile
(③は行の対応がずれやすいため、確認結果を見て手動で # を追加した方が確実な場合がある。基本方針は「ros_deep_learningpackage ブロックのRUN文をまるごとコメントアウトする」こと。)
手順4:ローカルビルド(docker/run.sh ではなく docker/build.sh)
docker/build.sh 2>&1 | tee ~/build.log
(ビルド完成品イメージはDocker Hubに存在しないため、必ずローカルビルドする。所要時間はJetson Nanoで1〜2時間程度)
手順5:モーター+OLED専用launchファイルを事前に用意
mkdir -p launch
cat > launch/jetbot_motors_only.launch.py << 'EOF'
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
motor_controller = Node(package='jetbot_ros', node_executable='motors_nvidia',
output='screen', emulate_tty=True)
oled_controller = Node(package='jetbot_ros', node_executable='oled_ssd1306',
output='screen', emulate_tty=True)
return LaunchDescription([
motor_controller,
oled_controller,
])
EOF
(このファイルはDockerfileの「build project」ステップで COPY launch ... されるため、ビルド前にリポジトリ内に置いておけば、ビルド後のイメージに最初から含まれる)
手順6:起動(2ターミナル構成)
ターミナルA(起動して放置):
docker/run.sh
# コンテナ内で:
ros2 launch jetbot_ros jetbot_motors_only.launch.py
ターミナルB(操作用、別途SSH接続):
sudo docker exec -it jetbot_ros /bin/bash
# コンテナ内で:
ros2 run jetbot_ros teleop_keyboard
(ros2 launch teleop_keyboard.launch.py は lxterminal=GUIを要求してSSH環境で失敗するため、必ず ros2 run で直接実行する)
手順7:カメラを使う場合(任意)
CSIカメラは生Bayer形式(RG10など)で出力されるため、v4l2_cameraは使わず、nvarguscamerasrcを使う独自ノードで配信する(コンテナ内、cv_bridgeはビルド不要で既存)。
# コンテナ内で
cat > ~/csi_camera_node.py << 'EOF'
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Image
from cv_bridge import CvBridge
import cv2
def gstreamer_pipeline(sensor_id=0, width=320, height=240, framerate=15, flip_method=2):
return (
f"nvarguscamerasrc sensor-id={sensor_id} ! "
f"video/x-raw(memory:NVMM), width=(int){width}, height=(int){height}, "
f"format=(string)NV12, framerate=(fraction){framerate}/1 ! "
f"nvvidconv flip-method={flip_method} ! "
f"video/x-raw, width=(int){width}, height=(int){height}, format=(string)BGRx ! "
f"videoconvert ! video/x-raw, format=(string)BGR ! appsink drop=1"
)
class CsiCameraNode(Node):
def __init__(self):
super().__init__('csi_camera')
self.publisher = self.create_publisher(Image, '/jetbot/camera/image_raw', 10)
self.bridge = CvBridge()
self.cap = cv2.VideoCapture(gstreamer_pipeline(), cv2.CAP_GSTREAMER)
if not self.cap.isOpened():
self.get_logger().error('カメラを開けませんでした(nvarguscamerasrc)')
return
self.timer = self.create_timer(1.0 / 15.0, self.timer_callback)
def timer_callback(self):
ret, frame = self.cap.read()
if ret:
msg = self.bridge.cv2_to_imgmsg(frame, encoding='bgr8')
self.publisher.publish(msg)
else:
self.get_logger().warn('フレーム取得に失敗しました')
def main(args=None):
rclpy.init(args=args)
node = CsiCameraNode()
rclpy.spin(node)
node.cap.release()
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
EOF
python3 ~/csi_camera_node.py
確認(別ターミナルのターミナルBで):
ros2 topic hz /jetbot/camera/image_raw
average rate: 15.0前後が安定して出れば成功。
1. git clone のURL指定ミス
症状:
git clone https://github.com
fatal: repository 'https://github.com/' not found
原因: git clone の後ろにリポジトリ名(dusty-nv/jetbot_ros)が抜けていた。
対処:
git clone https://github.com/dusty-nv/jetbot_ros
cd jetbot_ros
2. 既製Dockerイメージが存在しない問題
症状:
docker/run.sh 実行時:
Unable to find image 'dustynv/jetbot_ros:foxy-r32.7.1' locally
docker: Error response from daemon: manifest for dustynv/jetbot_ros:foxy-r32.7.1 not found
原因: dustynv/jetbot_ros:foxy-* の完成品イメージは、そもそもDocker Hubに公開されていない(dusty-nv/jetbot_ros リポジトリの既知の未解決issue #47と同様の事象)。docker/run.sh は自分のL4Tバージョンに応じてタグ名を自動生成するが、その組み合わせのビルド済みイメージは存在しない。
対処: docker/run.sh(pull)ではなく docker/build.sh(ローカルビルド)を使用する。
cd ~/jetbot_ros
docker/build.sh
3. ROS 2公式APT署名鍵の期限切れ
症状:
W: GPG error: http://packages.ros.org/ros2/ubuntu bionic InRelease:
The following signatures were invalid: EXPKEYSIG F42ED6FBAB17C654 Open Robotics
E: The repository 'http://packages.ros.org/ros2/ubuntu bionic InRelease' is not signed.
原因: Open Robotics(ROS公式)のAPT署名鍵が2025年6月頃に期限切れとなった、業界全体で発生している既知の問題。apt-key add による鍵の再取得だけでは解決しなかった(Ubuntu 18.04 bionic + ROS Foxyという組み合わせのEOL環境特有の事情)。
対処: 署名検証そのものを無効化する形で解決。/etc/apt/sources.list.d/ros2.list を以下のように書き換える。
echo 'deb [trusted=yes arch=arm64] http://packages.ros.org/ros2/ubuntu bionic main' > /etc/apt/sources.list.d/ros2.list
これをDockerfileの FROM ${BASE_IMAGE} 直後に追加することで、ビルドのたびに自動適用されるようにした:
FROM ${BASE_IMAGE}
RUN echo "deb [trusted=yes arch=arm64] http://packages.ros.org/ros2/ubuntu bionic main" > /etc/apt/sources.list.d/ros2.list
補足(つまずきポイント): sed で [trusted=yes] を挿入しようとした際、元の行にすでに [arch=arm64 signed-by=...] という角括弧があり、二重の角括弧になって Malformed entry(URI parse エラー)を起こした。最終的にはファイルを丸ごと1行で上書きする方式が確実だった。
4. メモリ不足によるビルド失敗(gazebo_ros)
症状:
c++: internal compiler error: Killed (program cc1plus)
make[2]: *** [test/CMakeFiles/test_gazebo_ros_factory.dir/test_gazebo_ros_factory.cpp.o] Error 4
原因: Jetson Nano本体のRAMが4GBしかなく、Dockerfileのデフォルト設定(ARG MAKEFLAGS=-j$(nproc)、CPUコア数ぶん並列コンパイル)でGazebo関連の重いC++ファイルを同時ビルドしようとし、メモリを使い切ってコンパイラプロセスが強制終了された。
対処:
① 並列ビルド数を1に制限:
sed -i 's|ARG MAKEFLAGS=-j\$(nproc)|ARG MAKEFLAGS=-j1|' Dockerfile
② スワップ領域を4GB追加:
sudo fallocate -l 4G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
5. ros_deep_learning とTensorRTのバージョン不整合
症状:
error: 'Dims3' in namespace 'nvinfer1' does not name a type
error: 'nvinfer1::IPluginFactory' has not been declared
error: 'nvinfer1::ICudaEngine' has not been declared
原因: ros_deep_learning(GitHubの最新ソースを毎回クローン)は古いTensorRT 7.x系のAPI(Dims3, IPluginFactoryなど)を前提に書かれているが、使用しているベースイメージには新しいTensorRTが入っており、該当クラスがすでに廃止されている。同じ作者(dusty-nv)のリポジトリ同士が経年劣化で噛み合わなくなっていた。
対処: JetBotの基本動作(モーター・カメラ制御)に ros_deep_learning(画像認識・物体検出用)は必須ではないため、ビルド対象から除外。
Dockerfile内の該当ブロック(119〜128行目付近)をコメントアウト:
sed -i '124,128s/^/#/' Dockerfile
#
# ros_deep_learning package
#
#RUN source ${ROS_ENVIRONMENT} && \
# cd ${WORKSPACE_ROOT}/src && \
# git clone https://github.com/dusty-nv/ros_deep_learning && \
# cd ../ && \
# colcon build --symlink-install --event-handlers console_direct+
6. ビルド成功・コンテナ起動
上記1〜5の対処後、40ステップすべて完走:
Successfully built 64f00bc04f8f
Successfully tagged jetbot_ros:foxy-r32.7.1
起動:
docker/run.sh
root@jetbot:/workspace# のプロンプトが出れば起動成功(xhost/xauth関連の警告は無視してよい)。
7. カメラ依存ノードによる連鎖クラッシュ
症状: ros2 launch jetbot_ros jetbot_nvidia.launch.py 実行時、OLED表示は一瞬成功するものの、しばらくすると motors_nvidia と oled_ssd1306 まで巻き添えで強制終了される。
PackageNotFoundError: "package 'ros_deep_learning' not found..."
AttributeError: 'NoneType' object has no attribute 'complete'
[ERROR] [motors_nvidia-1]: process has died
原因: jetbot_nvidia.launch.py の中に、5.で除外した ros_deep_learning パッケージの video_source / video_output ノードへの参照が残っていた。これが見つからずlaunchシステム全体が不安定になり、正常なノードにまで誤って SIGINT が送られていた。
対処: カメラノードを含まない、モーター+OLED専用のlaunchファイルを新規作成。
jetbot_motors_only.launch.py:
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
motor_controller = Node(package='jetbot_ros', node_executable='motors_nvidia',
output='screen', emulate_tty=True)
oled_controller = Node(package='jetbot_ros', node_executable='oled_ssd1306',
output='screen', emulate_tty=True)
return LaunchDescription([
motor_controller,
oled_controller,
])
起動:
ros2 launch jetbot_ros jetbot_motors_only.launch.py
これでOLEDのIP/CPU/メモリ表示が安定して継続し、モーターノードも落ちなくなった。
8. GUI(lxterminal)依存のteleop起動を回避
症状:
ros2 launch jetbot_ros teleop_keyboard.launch.py
Unable to init server: Could not connect: Connection refused
(lxterminal:4002): Gtk-WARNING **: cannot open display:
[ERROR] [teleop_keyboard-1]: process has died
原因: teleop_keyboard.launch.py はキー入力受付用に lxterminal(GUIターミナル)を新規に開く作りになっているが、SSH接続にはXディスプレイがないため起動できない。
対処: launchファイルを使わず、実行ファイルを直接、今のSSHターミナルの中で動かす。
ros2 run jetbot_ros teleop_keyboard
w/x(前進/後退)、a/d(左右旋回)、space/s(停止)のキー操作で モーター動作を確認。
作業の型(2ターミナル構成):
-
ターミナルA(PuTTYウィンドウ1):
docker/run.sh→ros2 launch jetbot_ros jetbot_motors_only.launch.pyを実行したまま放置(触らない) -
ターミナルB(PuTTYウィンドウ2、別途SSH接続):
sudo docker exec -it jetbot_ros /bin/bashでコンテナに追加接続し、そちらでros2 run jetbot_ros teleop_keyboardを実行
9. 作業内容の保存
① 動作中コンテナをDockerイメージとして固定
sudo docker commit jetbot_ros jetbot_ros:foxy-r32.7.1-stable
② 新規launchファイルをホスト側ソースにも反映
sudo docker cp jetbot_ros:/workspace/install/jetbot_ros/share/jetbot_ros/launch/jetbot_motors_only.launch.py ~/jetbot_ros/launch/jetbot_motors_only.launch.py
③ Dockerfileの変更をgitに記録
cd ~/jetbot_ros
git add Dockerfile launch/jetbot_motors_only.launch.py
git commit -m "Fix ROS2 apt key issue, limit build parallelism for low-memory Jetson Nano, disable incompatible ros_deep_learning package, add motors+OLED-only launch file"
④(推奨・任意)イメージのバックアップをファイルに書き出し
sudo docker save jetbot_ros:foxy-r32.7.1-stable -o ~/jetbot_ros_stable_backup.tar
SDカード破損に備え、PCなど別の場所にもコピーしておくと安心。
10. CSIカメラ対応(ros_deep_learning除外後の代替策)
5.でros_deep_learningをビルドから除外したため、カメラ機能が失われていた。ROS 2標準のv4l2_cameraパッケージへの切り替えを検討した。
まず確認:カメラの実際の出力形式
sudo apt-get install -y v4l-utils
v4l2-ctl --list-formats-ext -d /dev/video0
結果:
Pixel Format: 'RG10'
Name : 10-bit Bayer RGRG/GBGB
判明した問題: Waveshare JetBot AI KitのCSIカメラは、生の10-bit Bayer形式(RG10)でしか/dev/video0から出力されない。v4l2_cameraパッケージは一般的なUVC Webカメラの形式(YUYV/MJPEG)を前提としており、Bayer→RGB変換(ISP処理)を行わないため、そのままでは使えない。Jetsonでは、CSIカメラのISP処理はnvarguscamerasrc(GStreamer、Argusカメラデーモン経由)で行うのが標準的な方法。
対処:独自のROS 2ノードでnvarguscamerasrcを利用
ros_deep_learningのビルドをやり直す(TensorRT不整合の問題に再度直面する)のではなく、colconの再ビルドが不要な、単体のPythonスクリプトとして実装した。OpenCV(GStreamer対応でビルド済み)とcv_bridge(ベースイメージに既存、追加インストール不要)を使い、GStreamerパイプラインの映像をsensor_msgs/ImageとしてROS 2トピックに配信する。
~/csi_camera_node.py(コンテナ内で作成):
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Image
from cv_bridge import CvBridge
import cv2
def gstreamer_pipeline(sensor_id=0, width=320, height=240, framerate=15, flip_method=2):
return (
f"nvarguscamerasrc sensor-id={sensor_id} ! "
f"video/x-raw(memory:NVMM), width=(int){width}, height=(int){height}, "
f"format=(string)NV12, framerate=(fraction){framerate}/1 ! "
f"nvvidconv flip-method={flip_method} ! "
f"video/x-raw, width=(int){width}, height=(int){height}, format=(string)BGRx ! "
f"videoconvert ! video/x-raw, format=(string)BGR ! appsink drop=1"
)
class CsiCameraNode(Node):
def __init__(self):
super().__init__('csi_camera')
self.publisher = self.create_publisher(Image, '/jetbot/camera/image_raw', 10)
self.bridge = CvBridge()
self.cap = cv2.VideoCapture(gstreamer_pipeline(), cv2.CAP_GSTREAMER)
if not self.cap.isOpened():
self.get_logger().error('カメラを開けませんでした(nvarguscamerasrc)')
return
self.timer = self.create_timer(1.0 / 15.0, self.timer_callback)
def timer_callback(self):
ret, frame = self.cap.read()
if ret:
msg = self.bridge.cv2_to_imgmsg(frame, encoding='bgr8')
self.publisher.publish(msg)
else:
self.get_logger().warn('フレーム取得に失敗しました')
def main(args=None):
rclpy.init(args=args)
node = CsiCameraNode()
rclpy.spin(node)
node.cap.release()
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
実行:
python3 ~/csi_camera_node.py
nvarguscamerasrcが正常にArgusカメラへ接続すると、以下のようなログが出る(正常):
GST_ARGUS: Setup Complete, Starting captures for 0 seconds
GST_ARGUS: Starting repeat capture requests.
CONSUMER: Producer has connected; continuing.
(OpenCV | GStreamer warning: Cannot query video position という警告は、ライブストリームに対する無害な警告なので無視してよい)
動作確認
別ターミナルから配信レートを確認:
ros2 topic hz /jetbot/camera/image_raw
結果:
average rate: 15.000
min: 0.064s max: 0.070s std dev: 0.00069s window: 111
設定通り15fpsで安定配信を確認。
保存
# ホスト側にもファイルをコピー
sudo docker cp jetbot_ros:/root/csi_camera_node.py ~/jetbot_ros/csi_camera_node.py
# コンテナの状態を再度commit
sudo docker commit jetbot_ros jetbot_ros:foxy-r32.7.1-stable
今後のTODO候補
-
sudoなしでdocker操作: 毎回パスワード入力が必要な場合は、以下でdockerグループに追加すると省略可能:(実行後、一度ログアウト→再ログインが必要)sudo usermod -aG docker $USER -
Jetson Nanoのログインパスワード変更: 作業中に誤ってパスワード文字列がターミナル出力に混入したことがあったため、念のため
passwdコマンドでの変更を推奨。