Nvidia Jetson Thor による reBot Arm B601 の音声制御
はじめに
大規模言語モデル(LLM)は、自然言語での対話を可能にすることで、ロボットシステムをより直感的なものにしています。ロボットビジョンや把持計画と組み合わせることで、ユーザーはシンプルな音声コマンドでロボットを操作できます。

このチュートリアルでは、NVIDIA Jetson Thor プラットフォーム上に、完全な音声制御ロボット把持システムをデプロイします。
特長
- Jetson Thor 上での完全ローカル AI 推論
- Whisper を用いた音声インタラクション
- Ollama によるローカル LLM
- ブラウザベースの OpenWebUI インターフェース
- GraspNet を用いた 6 自由度把持検出
- ロボットアームのリアルタイム制御
ワークフロー

ハードウェアの準備
必要なハードウェア
- Nvidia Jetson Thor(JetPack 7.x インストール済み)
- reBot Arm B601 DM
- USB-to-CAN アダプタ
- Orbbec Gemini 2 RGBD カメラ
- reSpeaker
- ロボットアーム用電源および USB ケーブル
- USB ボタン(オプション)
| reBot Arm B601 | NVIDIA® Jetson AGX Thor™ Developer Kit | Orbbec Gemini 2 RGBD Camera | reSpeaker Flex XVF3800 Linear-4 |
|---|---|---|---|
![]() | ![]() | ![]() | ![]() |
ハードウェア接続
- カメラを Jetson Thor の USB ポートに接続します。
- CAN2USB モジュールを介して reBot Arm RS を Jetson Thor に接続します。
- USB マイクを Jetson Thor に接続します。
- すべてのデバイスの電源を入れます。
Ollama のインストール
ステップ 1. Ollama をインストールする
curl -fsSL https://ollama.com/install.sh | sh
ステップ 2. 言語モデルをダウンロードする
ollama pull nemotron3:33b
ステップ 3. Ollama を確認する
ollama run nemotron3:33b
OpenWebUI のデプロイ
ステップ 1. OpenWebUI を起動する
docker run -d -p 4000:8080 \
--add-host=host.docker.internal:host-gateway \
-e OLLAMA_BASE_URL=http://host.docker.internal:11434 \
-v open-webui_tt:/app/backend/data --name open-webui_tt \
ghcr.io/open-webui/open-webui:main
注意: 公式の Open WebUI Docker イメージには、プッシュ・トゥ・トーク(ボタンでトリガーされる音声録音)機能は含まれていません。プッシュ・トゥ・トーク機能を有効にしたい場合は、代わりに次の Docker イメージを使用してください。
ステップ 2. インターフェースへアクセスする
ブラウザを開き、次の URL にアクセスします:
http://<Jetson-IP>:3000
ステップ 3. Ollama を接続する
OpenWebUI 内で Ollama エンドポイントを設定します。
http://host.docker.internal:11434
Open WebUI が Ollama に正しく接続できない場合は、Ollama の systemd サービス設定を変更し、その後 Ollama サービスを再起動してください。 /etc/systemd/system/ollama.service
[Unit]
Description=Ollama Service
After=network-online.target
[Service]
ExecStart=/usr/local/bin/ollama serve
User=ollama
Group=ollama
Restart=always
RestartSec=3
Environment="PATH=/home/seeed/.local/bin:/home/seeed/.local/bin:/home/seeed/.nvm/versions/node/v22.22.2/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin"
Environment="OLLAMA_HOST=0.0.0.0:11434"
[Install]
WantedBy=default.target
ステップ 4. ツールを設定する
Workspace --> Tools --> New Tool
robot arm tools
"""
title: Grasp Web Robot Arm
author: seeedstudio
version: 0.3.0
description: Open WebUI tools for controlling grasp_web.py robot arm HTTP API.
requirements: requests
"""
import os
import requests
from typing import Dict, Any, List, Optional
class Tools:
def __init__(self):
self.base_url = os.getenv(
"GRASP_BASE", "http://host.docker.internal:8090"
).rstrip("/")
self.timeout = float(os.getenv("GRASP_TIMEOUT", "20"))
def _json_or_text(self, r: requests.Response):
try:
return r.json()
except Exception:
return r.text
def _get(self, path: str) -> Dict[str, Any]:
try:
r = requests.get(f"{self.base_url}{path}", timeout=self.timeout)
return {
"ok": r.ok,
"status_code": r.status_code,
"data": self._json_or_text(r),
}
except Exception as e:
return {"ok": False, "error": str(e)}
def _post(self, path: str, payload: Optional[dict] = None) -> Dict[str, Any]:
try:
r = requests.post(
f"{self.base_url}{path}",
json=payload or {},
timeout=self.timeout,
)
return {
"ok": r.ok,
"status_code": r.status_code,
"data": self._json_or_text(r),
}
except Exception as e:
return {"ok": False, "error": str(e)}
# -------------------------
# 状态查询
# -------------------------
def robot_state(self) -> Dict[str, Any]:
"""
读取机器人状态:关节角度、末端位姿、夹爪状态,包括位置、速度、力矩、是否夹住。
这是只读操作,不会驱动机械臂。
"""
return self._get("/robot/state")
# -------------------------
# 基础操作
# -------------------------
def reset_robot(self) -> Dict[str, Any]:
"""
复位机器人:停止当前执行、松开夹爪、机械臂回原点。
注意:这会驱动真实机械臂运动。
"""
return self._post("/reset", {})
def move_ready(self) -> Dict[str, Any]:
"""
移动机械臂到预定义就绪位:张开夹爪并移动到就绪姿态。
注意:这会驱动真实机械臂运动。
"""
return self._post("/ready", {})
# -------------------------
# 目标类别设置
# -------------------------
def set_target_class(self, class_name: str) -> Dict[str, Any]:
"""
设置目标类别,例如 bottle、cup、apple。
设置后后端会自动触发推理更新。
传入空字符串 "" 可以取消类别过滤,扫描全场所有检测到的物体。
"""
if class_name is None:
return {"ok": False, "error": "class_name must be a string"}
return self._post("/target", {"class_name": class_name})
def clear_target_filter(self) -> Dict[str, Any]:
"""
取消目标类别过滤,扫描全场所有检测到的物体。
"""
return self._post("/target", {"class_name": ""})
# -------------------------
# 夹爪控制
# -------------------------
def gripper_state(self) -> Dict[str, Any]:
"""
读取夹爪状态:位置、速度、力矩、是否夹住。
这是只读操作,不会驱动机械臂。
"""
return self._post("/gripper", {"action": "state"})
def gripper_open(self, distance_m: float = 0.09) -> Dict[str, Any]:
"""
张开夹爪到指定距离。
参数:
- distance_m: 张开距离,单位米,默认 0.09
注意:这会驱动真实夹爪。
"""
if distance_m < 0:
return {"ok": False, "error": "distance_m must be >= 0"}
return self._post(
"/gripper",
{
"action": "open",
"distance_m": distance_m,
},
)
def gripper_close(self) -> Dict[str, Any]:
"""
闭合夹爪,使用非阻塞力矩模式。
注意:这会驱动真实夹爪。
"""
return self._post("/gripper", {"action": "close"})
def gripper_release(self) -> Dict[str, Any]:
"""
释放夹爪,松开并解除 HOLDING 状态。
注意:这会驱动真实夹爪。
"""
return self._post("/gripper", {"action": "release"})
# -------------------------
# 关节控制
# -------------------------
def joint_jog(
self,
joint: str,
delta_deg: float,
duration_s: float = 2.0,
safety_margin_deg: float = 5.0,
) -> Dict[str, Any]:
"""
单关节点动控制。
参数:
- joint: joint1 ~ joint6
- delta_deg: 相对旋转角度,单位度。正数为正方向,负数为负方向
- duration_s: 运动时间,单位秒
- safety_margin_deg: 限位安全边距,单位度
注意:这会驱动真实机械臂。
"""
valid_joints = {"joint1", "joint2", "joint3", "joint4", "joint5", "joint6"}
if joint not in valid_joints:
return {"ok": False, "error": "joint must be one of joint1 ~ joint6"}
if duration_s <= 0:
return {"ok": False, "error": "duration_s must be greater than 0"}
if safety_margin_deg < 0:
return {"ok": False, "error": "safety_margin_deg must be >= 0"}
return self._post(
"/joint/jog",
{
"joint": joint,
"delta_deg": delta_deg,
"duration_s": duration_s,
"safety_margin_deg": safety_margin_deg,
},
)
def move_joints(
self,
joints_rad: List[float],
duration_s: float = 3.0,
) -> Dict[str, Any]:
"""
一次性设置所有 6 个关节的绝对位置。
参数:
- joints_rad: 6 个关节角,单位弧度
- duration_s: 运动时间,单位秒
关节顺序:
[joint1, joint2, joint3, joint4, joint5, joint6]
注意:这会驱动真实机械臂。
"""
if not isinstance(joints_rad, list) or len(joints_rad) != 6:
return {"ok": False, "error": "joints_rad must be a list of 6 numbers"}
if duration_s <= 0:
return {"ok": False, "error": "duration_s must be greater than 0"}
return self._post(
"/joint/move",
{
"joints_rad": joints_rad,
"duration_s": duration_s,
},
)
def move_zero_joints(self) -> Dict[str, Any]:
"""
将所有关节移动到 0 位。
注意:这会驱动真实机械臂。
"""
return self.move_joints(
joints_rad=[0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
duration_s=3.0,
)
def move_ready_joints(self) -> Dict[str, Any]:
"""
移动到示例就绪关节位:
[0.0, -1.0, -1.5, 0.5, 0.0, 0.0]
注意:这会驱动真实机械臂。
"""
return self.move_joints(
joints_rad=[0.0, -1.0, -1.5, 0.5, 0.0, 0.0],
duration_s=3.0,
)
# -------------------------
# 末端位姿控制
# -------------------------
def move_pose(
self,
x: float,
y: float,
z: float,
roll: float,
pitch: float,
yaw: float,
duration: float = 3.0,
) -> Dict[str, Any]:
"""
末端位姿控制。服务端会先做 IK 规划,不可达则返回错误。
参数:
- x, y, z: 末端位置,单位米
- roll, pitch, yaw: 末端姿态,单位弧度
- duration: 运动时间,单位秒
建议先调用 ready 或 joint/move 到可达位置后,再使用 move_pose。
注意:这会驱动真实机械臂。
"""
if duration <= 0:
return {"ok": False, "error": "duration must be greater than 0"}
return self._post(
"/move/pose",
{
"x": x,
"y": y,
"z": z,
"roll": roll,
"pitch": pitch,
"yaw": yaw,
"duration": duration,
},
)
def move_ready_pose(self) -> Dict[str, Any]:
"""
移动到默认末端就绪位:
x=0.25, y=0.0, z=0.35, roll=0.0, pitch=1.2, yaw=0.0
注意:这会驱动真实机械臂。
"""
return self.move_pose(
x=0.25,
y=0.0,
z=0.35,
roll=0.0,
pitch=1.2,
yaw=0.0,
duration=3.0,
)
def move_near_current_pose(self) -> Dict[str, Any]:
"""
移动到从零位出发通常可达的保守末端位:
x=0.260, y=0.0, z=0.20, roll=0.0, pitch=0.0, yaw=0.0
注意:这会驱动真实机械臂。
"""
return self.move_pose(
x=0.260,
y=0.0,
z=0.20,
roll=0.0,
pitch=0.0,
yaw=0.0,
duration=2.0,
)
# -------------------------
# 底座点动
# -------------------------
def base_jog(
self,
delta_deg: float,
duration_s: float = 2.0,
) -> Dict[str, Any]:
"""
底座点动。
参数:
- delta_deg: 底座旋转角度,单位度。正数为正方向,负数为负方向
- duration_s: 运动时间,单位秒
注意:这会驱动真实机械臂底座。
"""
if duration_s <= 0:
return {"ok": False, "error": "duration_s must be greater than 0"}
return self._post(
"/base_jog",
{
"delta_deg": delta_deg,
"duration_s": duration_s,
},
)
# -------------------------
# 一键自动抓取
# -------------------------
def auto_grasp(
self,
class_name: str = "bottle",
max_retries: int = 10,
retry_interval_s: float = 1.5,
) -> Dict[str, Any]:
"""
一键自动抓取。
流程:
1. 设置目标类别
2. 自动循环推理
3. 检测到目标后执行夹取
后端会在后台运行抓取流程。
可通过 robot_state 查看进度。
参数:
- class_name: 目标类别,例如 bottle。空字符串表示扫描全场
- max_retries: 最大重试次数
- retry_interval_s: 每次重试间隔,单位秒
注意:这会驱动真实机械臂。
"""
if max_retries <= 0:
return {"ok": False, "error": "max_retries must be greater than 0"}
if retry_interval_s <= 0:
return {"ok": False, "error": "retry_interval_s must be greater than 0"}
return self._post(
"/auto_grasp",
{
"class_name": class_name,
"max_retries": max_retries,
"retry_interval_s": retry_interval_s,
},
)
# -------------------------
# 视频流
# -------------------------
def stream_url(self) -> Dict[str, Any]:
"""
返回 MJPEG 视频流地址。
不会驱动机械臂。
"""
return {
"ok": True,
"stream_url": f"{self.base_url}/stream.mjpg",
}
reBot Arm バックエンドサービスをデプロイする
ステップ 1. システムパッケージと Miniconda をインストールする
sudo apt update
sudo apt install -y git wget curl build-essential cmake libusb-1.0-0-dev python3-pip
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-aarch64.sh
bash Miniconda3-latest-Linux-aarch64.sh
source ~/.bashrc
ステップ 2. プロジェクトをクローンする
git clone https://github.com/jjjadand/reBot-DevArm-Grasp.git rebot_grasp-jetson
cd rebot_grasp-jetson
公開リポジトリではなく社内パッケージを使用している場合は、そのプロジェクトディレクトリを Jetson にコピーし、残りのコマンドはプロジェクトルートから実行してください。
ステップ 3. Python 環境を作成する
JetPack 6.x では Python 3.10 を使用します。JetPack 7.x / Thor では Python 3.12 を使用します。
# JetPack 6.x
conda create -y -n graspnet python=3.10
# JetPack 7.x / Thor
# conda create -y -n graspnet python=3.12
conda activate graspnet
python -m pip install -U pip wheel setuptools
JetPack と CUDA のバージョンを確認します:
cat /etc/nv_tegra_release
nvcc --version
ステップ 4. Jetson 対応の PyTorch をインストールする
Jetson には汎用の PyPI CPU/GPU 用 PyTorch パッケージをインストールしないでください。JetPack、Python、CUDA のバージョンに一致する wheel をインストールします。reComputer ユーザーは、専用ガイド「Install Pytorch for reComputer Jetson」に従うこともできます。
一般的な出発点は次のとおりです:
# JetPack 6.x, CUDA 12.x, Python 3.10
pip install --extra-index-url https://pypi.jetson-ai-lab.io/jp6/cu126 torch torchvision
# JetPack 7.x / Thor, CUDA 13.x, Python 3.12
# pip install --extra-index-url https://pypi.jetson-ai-lab.io/sbsa/cu130 torch torchvision
Python から CUDA を検証します:
python - <<'PY'
import torch
print("torch:", torch.__version__)
print("cuda available:", torch.cuda.is_available())
print("device:", torch.cuda.get_device_name(0) if torch.cuda.is_available() else "none")
PY
続行する前に、cuda available が True である必要があります。
ステップ 5. Python 依存パッケージをインストールする
pip install -r requirements-graspnet-jetson.txt
ステップ 6. ロボット、GraspNet、および GraspNet API SDK をインストールする
mkdir -p sdk
git clone https://github.com/vectorBH6/reBotArm_control_py.git sdk/reBotArm_control_py
pip install -e sdk/reBotArm_control_py
git clone https://github.com/graspnet/graspnet-baseline.git sdk/graspnet-baseline
git clone https://github.com/graspnet/graspnetAPI.git sdk/graspnetAPI
pip install -e sdk/graspnetAPI
GraspNet のダウンロードページから GraspNet の事前学習済みチェックポイントをダウンロードし、ここに配置します:
sdk/graspnet-baseline/checkpoints/checkpoint-rs.tar
ステップ 7. CUDA パスを設定し、GraspNet CUDA オペレータをビルドする
JetPack のバージョンに応じて CUDA パスを設定します:
# JetPack 6.x example
export CUDA_HOME=/usr/local/cuda-12.6
# JetPack 7.x / Thor example
# export CUDA_HOME=/usr/local/cuda-13.0
export PATH="$CUDA_HOME/bin:$PATH"
export LD_LIBRARY_PATH="$CUDA_HOME/lib64:$LD_LIBRARY_PATH"
GraspNet が使用する CUDA 拡張をビルドします:
bash scripts/install_graspnet_cuda_ops.sh
bash scripts/install_graspnet_cuda_ops.sh --check
後で JetPack、Python、CUDA、または PyTorch を変更した場合は、次で再ビルドします:
bash scripts/install_graspnet_cuda_ops.sh --force
ステップ 8. Orbbec カメラ SDK をインストールする
このプロジェクトには、アクティブな Python 環境に pyorbbecsdk2 をインストールするヘルパーが含まれています:
bash scripts/install_pyorbbecsdk.sh
SDK をローカルでビルドする必要がある場合は、まずソースツリーをクローンし、ソースモードを実行します:
git clone https://github.com/orbbec/pyorbbecsdk.git sdk/pyorbbecsdk
bash scripts/install_pyorbbecsdk.sh --from-source
SDK ソースツリーが利用可能なときに Orbbec の udev ルールをインストールします:
sudo bash sdk/pyorbbecsdk/scripts/env_setup/install_udev_rules.sh
sudo udevadm control --reload-rules
sudo udevadm trigger
ステップ 9. YOLO の重みをダウンロードし、対象 Jetson 上で TensorRT をエクスポートする
TensorRT エンジンファイルはデバイス固有です。デモを実行する Jetson 上で .engine を必ずエクスポートしてください。
mkdir -p models
wget https://github.com/ultralytics/assets/releases/download/v8.3.0/yolo11n-seg.pt -O models/yolo11n-seg.pt
yolo export model=models/yolo11n-seg.pt format=engine imgsz=640 half=True device=0 workspace=4
期待される出力は次のとおりです:
models/yolo11n-seg.engine
プラットフォームで FP16 エクスポートが失敗する場合は、half=True なしでエクスポートします:
yolo export model=models/yolo11n-seg.pt format=engine imgsz=640 device=0 workspace=4
検証とキャリブレーション
実際に把持を試みる前に、次のチェックを順番に実行してください。
1. RGB-D カメラを確認する
conda activate graspnet
cd ~/rebot_grasp-jetson
python scripts/verify_pyorbbec_stream.py
python scripts/verify_pyorbbec_stream.py --preview --seconds 10
テキストのみのチェックでは、RGB と深度フレームの情報が出力されるはずです。プレビューのチェックでは、デスクトップディスプレイが利用可能な場合に RGB と深度のウィンドウが表示されるはずです。
2. ロボット接続を確認する
まず読み取り専用モードから開始します:
python scripts/verify_rebot_arm_motion.py --read-only
アームの動作経路が安全であることを確認したら、joint6 を少しジョグさせます:
python scripts/verify_rebot_arm_motion.py --deg 5
3. GraspNet スタックを確認する
python scripts/verify_graspnet_stack.py
カメラがまだ接続されていないが、Python、CUDA、GraspNet、および YOLO ファイルだけを確認したい場合:
python scripts/verify_graspnet_stack.py --skip-camera
4. アイインハンドキャリブレーションを実行する
このプロジェクトではアイインハンドキャリブレーションを使用します:カメラはエンドエフェクタに取り付けられ、ArUco マーカーはテーブル上に固定されます。デフォルト設定では、DICT_4X4_50、ID 0、0.1 m のマーカーを想定しています。リポジトリには aruco100x100.pdf などの印刷可能なマーカーファイルが含まれています。
自動収集:
python scripts/collect_handeye_eih.py
重力補償付きの手動収集:
python scripts/collect_handeye_eih.py --manual
キャリブレーション結果は、アクティブなカメラディレクトリ配下に保存されます:
config/calibration/orbbec_gemini2/hand_eye.npz
config/calibration/orbbec_gemini2/intrinsics.npz
保存されたキャリブレーションを検証します:
python scripts/verify_handeye_calibration.py
カメラマウント、グリッパ、ArUco ボードサイズ、またはテーブル形状が変わるたびに再キャリブレーションしてください。
アプリケーションを起動する
最初のユーザーインターフェースとしては Web UI を推奨します。Web UI は、ライブ MJPEG ビデオ、ターゲット選択、把持プレビュー、実際の把持実行、補正調整、ベースジョグ、グリッパ制御、レディポーズ、およびリセット操作を提供します。
まずプレビューモードで起動します。これは実際のロボット動作は行いません:
conda activate graspnet
cd ~/rebot_grasp-jetson
python scripts/grasp_web.py \
--host 0.0.0.0 \
--port 8000 \
--num-point 12000 \
--cloud-crop-nsample 32
ブラウザから Web UI を開きます:
http://<jetson_ip>:8000
プレビューモードを使用して、カメラストリーム、YOLO 検出、ターゲットフィルタリング、および把持点生成を確認します。Web UI の infer または refresh コントロールをクリックして、GraspNet プレビューを更新します。
Open WebUI ページ(http://<jetson_ip>:4000)を開きます。チャットで Robot Arm Tools を有効にしている場合は、自然言語コマンドを使用してロボットアームを直接制御できます。
機能デモ
参考資料
- https://docs.nvidia.com/jetson/agx-thor-devkit/user-guide/latest/index.html
- https://github.com/Seeed-Projects/reBot-DevArm
- https://github.com/graspnet/graspnet-baseline.git
- https://github.com/vectorBH6/reBotArm_control_py
- https://github.com/orbbec/pyorbbecsdk.git
- https://wiki.seeedstudio.com/ja/install_torch_on_recomputer/
技術サポート & 製品ディスカッション
弊社製品をお選びいただきありがとうございます。弊社は、製品をできるだけスムーズにご利用いただけるよう、さまざまなサポートを提供しています。お好みやニーズに合わせて選べる複数のコミュニケーションチャネルをご用意しています。



