歩道を走る自動配送ロボットをCube Orange / Jetson Orin / ROS 2で組み上げた
市販の4輪スキッドステア台車 AgileX Scout Mini に、フライトコントローラ Cube Orange(ArduRover) と Jetson AGX Orin (ROS2) を載せ、歩道を自律走行する配送ロボットを開発しています。
2026年11月からは、岡山大学様のサポートを受けながら、岡山県吉備中央町で公道実証実験を実施しています。
この記事では、システム全体の構成と、その構成に至った理由、そして実際に歩道を走らせて分かったことをまとめます。ポールにぶつけた話や、枯れ草に89回止められた話も書きます。次の動画は、公道で行った検証実験の様子です。

記事中の数値は執筆時点(2026年11月)のリポジトリの設定値です。「実機未確認」と書いた機能は、実装と単体テストは済んでいるものの、走行での確認がまだのものです。
1. 設計の方針: 走るのは Cube、止めるのは Jetson
最初に決めたのは役割分担です。
- Cube Orange(ArduRover) がミッション(ウェイポイント列)を持ち、AUTO モードで経路追従する。
- Jetson はセンサーを見て、危ないときに Cube を HOLD にし、必要なら GUIDED で障害物を回り込み、また AUTO に返す。
- 駆動ブリッジ は Cube のモーター出力(PWM)を Scout Mini の CAN 指令に変換するだけ。
1.1. 事前の3D点群地図を作らずに走る
このロボットの特徴は、走行前にLiDARなどで現地を計測し、3D点群地図を作成しておく必要がないことです。事前に与えるのは、GNSSの緯度・経度で構成したルート情報(ウェイポイント列)だけです。

走行中はそのルートを基準にしながら、LiDAR、深度カメラ、ミリ波レーダー、超音波センサーで周囲を観測します。経路上に人や車両、草、ポールなどがあれば、その場で減速・停止・待機・局所迂回を判断します。つまり、固定された事前地図へ周囲を照合して走るのではなく、GNSSで進行ルートを把握し、現地の状況は搭載センサーで判断する構成です。
ROS 2 の Nav2 でグローバル経路から全部を組む構成も考えられますが、次の理由でこの形にしました。
- ArduRover の経路追従・EKF・フェイルセーフ・Mission Planner での運用は実績があり、作り直す理由がない。
- Jetson 側のソフトが落ちても、Cube とブリッジだけで「止まる」ことができる(後述のインターロック)。
- Jetson の仕事を「止める・避ける・返す」に絞れるので、判断の筋道をログから追いやすい。
Jetson は Cube のアーム操作も、パラメータの書き換えも原則しません。やるのはモード要求(HOLD / GUIDED / AUTO)と、GUIDED 中の速度・角速度指令、そして障害物距離の送信です。
2. ハードウェア構成
2.1. 車体と計算機
| 部位 | 機材 | 役割 |
|---|---|---|
| 車体 | AgileX Scout Mini(4輪スキッドステア、車幅 0.58 m) | 駆動。CAN で速度・角速度指令を受ける |
| 経路追従 | Cube Orange + ArduRover | ミッション、EKF、モード管理 |
| 測位 | GNSS 2アンテナ(RTK、2アンテナ方位) | 位置と方位 |
| 計算機 | Jetson AGX Orin(12コア / 32GB 級、JetPack 6系) | 認識・判断・ブリッジ・記録のすべて |
駆動ブリッジは当初 Raspberry Pi で動かしていましたが、途中で Jetson に統合しました。機体上の計算機は Jetson 1台です。
2.2. センサー
| センサー | 機材 | 主な用途 |
|---|---|---|
| 2D LiDAR | RPLIDAR S2 | 全周の障害物、局所経路の「空き」の根拠 |
| 深度カメラ(前方) | RealSense(水平設置) | 前方の障害物、路面推定、信号機認識、路面セグメンテーション |
| 深度カメラ(下向き) | RealSense(俯角15度) | 足元の低い障害物、段差・穴、縁石 |
| ミリ波レーダー | T18PE(79GHz) | 前方の移動物の検出 |
| 超音波 ×4 | A02YYUW(左・右・左後・右後) | 至近距離の側方・後方 |
| 左右カメラ ×2 | USB カメラ(真横向き) | 交差点での接近車両の確認 |
| 信号機用カメラ | Logicool C270(前向き) | 一時停止地点での信号機・踏切警報灯。内蔵マイクは警報音と遠隔通話に利用 |
| 後方カメラ | U20CAM-1080p | 後退前の人・車の確認 |
| 外部 IMU | TR-IMU | 推測航法の比較・検証用 |
USB の接続先は udev で /dev/sensors/lidar-s2 のような固定名にしています。FTDI 製の USB シリアルには個体シリアルがないものがあり、その場合は物理ポートの位置で識別しています。
3. ソフトウェア構成
3.1. プロセスの単位は systemd サービス
機体のソフトは systemd サービスとして常駐します。起動順は mavlink-router-scout → scout-cameras → scout-fusion です。
| サービス | 内容 |
|---|---|
mavlink-router-scout | Cube の UART を唯一開くプロセス。TCP で他へ中継 |
scout-bridge | C++ 製の駆動ブリッジ。Cube の PWM → CAN |
scout-cameras | RealSense 2台のドライバ(コンポーネントコンテナ) |
scout-fusion | センサー融合・停止判定・局所迂回・自己位置推定・各種認識 |
scout-field-recorder | 実走行ログの記録 |
scout-camera-stream | 遠隔監視用の映像・音声配信(走行判断には無関係) |
scout-notifier | イベント通知(Telegram) |
UART を開くのは mavlink-router だけ、という原則を最初に決めました。ブリッジも ROS ノードも Mission Planner も、すべて TCP のクライアントです。誰がシリアルを掴んでいるか分からなくなる事故を構造的に避けられます。
3.2. ROS 2 ノード
ROS 2 側の本体は scout_perception パッケージ(Python、約80モジュール・2.5万行)です。
中核は proximity_feed ノードです。センサー融合、停止判定、迂回制御器(DetourControl)、Cube との MAVLink 通信を1つのノードに持たせています。判断に関わる状態を1か所に集め、「なぜ止まったか」を1つの状態トピック(stop_status)に出すためです。
判断ロジック本体(停止領域、障害物を避ける局所経路探索、MPPI、接近車両判定など)は ROS に依存しない純粋な Python モジュールとして書き、ノードは入出力だけを担当します。これが後述の単体テスト・ログ再生・シミュレータでの再利用を支えています。
4. 駆動ブリッジ: PWM を CAN に変える
ArduRover はスキッドステアの左右モーター出力を PWM で出します。Scout Mini は CAN で「前後速度・角速度」を受けます。この間をつなぐのが scout_bridge(C++)です。
- Cube の
SERVO_OUTPUT_RAW(左右の PWM)を 10Hz で受け、正規化して速度・角速度へ変換し、50Hz で CAN へ送る。 - MAVLink の外部ライブラリには依存せず、必要なメッセージだけの最小実装を内蔵。
- 入力が 500ms 途絶えたらゼロ指令。途絶が続けば「モーションラッチ」を落とし、明示的に再有効化するまで動かさない。
- 動いてよいのは MANUAL / AUTO / GUIDED で、かつアーム中、かつラッチ有効のときだけ。
- 加減速に上限を付け、障害物停止のときに急制動で荷物を倒さないようにする(AUTO/HOLD の減速は 0.5〜0.8 m/s² 級)。
ブリッジは「Cube が言ったとおりに動かす」だけの薄い層ですが、止める方向の入力だけは別経路で受け付けます。その代表が次章以降で出てくる健全性インターロックです。
5. 障害物検知と停止判定
5.1. 72区画への融合
LiDAR、前方・下向きカメラ、レーダーの検出結果を、車体中心まわりの 5度刻み72区画 の距離に融合します。これは MAVLink の OBSTACLE_DISTANCE の形式そのままで、約10Hz で Cube にも送っています(Mission Planner の近接表示に出ます)。
ただし、止める判断は Cube の近接回避機能に任せず Jetson がします。Cube 側の回避は全周に一律のマージンを取るため、歩道では横の壁や植え込みで止まってしまうからです。
5.2. 止める領域
AUTO で走行中、次の領域に検出があれば HOLD を要求します。
- 正面 ±15度・半径 1.5 m の扇形
- 前方 2.5 m × 左右 ±0.39 m(車体半幅 0.29 m + 余裕 0.10 m)の通過範囲
- 次の WP へ向きを変える途中なら、現在の向きから WP 方向までを掃く範囲
横や後ろに物が「あるだけ」では止まりません。歩道は横に物があるのが普通だからです。
さらに、停止領域の手前 1.5 m に入った時点で AUTO の速度を 0.35 m/s まで落とします。巡航 0.75 m/s からいきなり止まるより、制動距離にも荷物にも優しいためです。
5.3. カメラ深度の扱い
深度カメラは「路面から何 cm 出ているか」を知るために、毎フレーム路面の平面を推定しています。ここが屋外では手強い部分でした。
- 推定に使う画像の範囲に路面以外が多く入ると平面が決まらない → ROI を下側30%・中央幅40% に絞った。
- 強い影で平面推定が狂い、ありもしない低い障害物が出る → 平面の品質判定を追加。
- 1フレームのノイズで止まらないよう、カメラごとに2回連続の検出で確定。
5.4. センサーが黙ったら止まる
必須センサーの処理結果が設定した鮮度閾値より古くなったら、障害物がなくても HOLD します。「見えていない」と「何もない」を混同しないための基本ルールです。レーダーは検出ゼロのときも空フレームを配信させ、無検出と故障を区別しています。
LiDAR は USB の瞬断などで無応答になることがあり、ウォッチドッグノードがドライバを再起動します(試験では停止から19秒で自動復旧)。
6. 止まったあと: 待機・局所迂回・復帰
止まるだけなら簡単です。難しいのは「止まったあとどうするか」でした。
6.1. 待つ
HOLD 後、まず5秒待ちます。相手が歩行者なら、たいていその間にいなくなります。レーダーが前方の移動物(0.2 m/s 以上)を検出している間は、待機時間を過ぎても迂回を始めません。動いている人を避けに行くより、通り過ぎるのを待つ方が安全で、結果的に早いからです。
一方、止まった原因が路面の低い物だけ(LiDAR にもレーダーにも映らず、カメラの点がすべて低い)なら待つ意味がないので、待機を省いてすぐ計画に進みます。
6.2. 計画する: 「観測済みの空き」だけで迂回経路を探す
局所経路は、LiDAR が実際に測距して空いていると確認できた領域だけを使い、障害物を避ける経路を探索して引きます。未観測の場所は通りません。障害物の陰で合流点が見えないときは、見えている側方の中継点まで進んでから計画し直します。
車体モデルは2段構えです。
- まず円形(回転しても当たらない安全側)で外側を回る経路を探す。
- ポールの間など円では通れないとき、または隙間を抜けた方が明らかに短いときは、実寸の矩形で計画する。このとき障害物は格子セルに丸めず、検出点そのもので当たり判定する。
格子への丸めは侮れません。0.15 m のセルに丸めると、内法 0.9 m のポールの間は横ずれ 7.5 cm で「通れない」と判定されました。点で判定することで、0.82〜0.9 m 級の隙間を通過できるようになりました。
6.3. 追従する: MPPI
経路への追従は MPPI(Model Predictive Path Integral) です。Nav2 の MPPI と同型のサンプリング予測制御を、ROS に依存しない形で自前実装しています。
- 速度・角速度の系列をランダムに多数生成して数秒先まで予測し、コストの低いものを重み付き平均する。
- コストは、ゴール・参照経路への近さ、進捗、障害物への余裕、旋回の滑らかさなど。
- 未観測域への侵入と車体の接触は「コスト」ではなく、破ってはいけない硬い制約。
- 前進する左右の円弧を必ず候補に含め、「止まっているのが一番安い」解に張り付くのを防ぐ。
計画は重い処理なので別プロセスに逃がし、結果が届くまでは今の経路を追い続けます。計画の遅れは障害物ではないので、遅れただけでは異常停止にしません。
6.4. 返す
迂回の最終目標は、合流点ではなく今のミッション WP そのものです。WP の受理円(1.1 m。Cube の WP_RADIUS 1.2 m より少し内側)まで GUIDED で進み、実際に停止したことを確認して AUTO に返します。Cube はその WP を受理して次へ進むので、ミッション番号をいじる必要がありません。
6.5. それでも詰まったら
- 袋小路: 来た道の軌跡を記録しておき、前と両側が塞がったら最大 0.1 m/s で後退して、側方が開けた地点から計画し直す。後退は後部超音波と後方カメラ(人・車の検出)が許すときだけ。
- 側方の至近物: 超音波が片側だけ近いときは、少し後退して反対側へ向き直ってから計画。
- 再試行の上限: 同じ WP で3回失敗したら異常停止として HOLD を保持し、人を呼ぶ。
無限に頑張らせない、というのも設計のうちです。
7. 交差点・信号・踏切
道路を渡る場所では、ミッションの WP を LOITER_UNLIM(無期限待機) にしておきます。Cube はこの WP から自分では進みません。Jetson が「進んでよい」と判断したときだけ、次の WP へ進めます。判断できなければ止まったまま、という安全側の作りです。
7.1. 信号機
信号機の「位置の検出」と「色の分類」は別モデルです。Autoware Universe の公開モデル(YOLOX による車両用/歩行者用信号の検出と、MobileNetV2 による日本の信号の色分類)を TensorRT(FP16)で動かしています。歩行者用信号を優先し、見つからないときに車両用を見ます。
実地で苦労したのはモデルより露出でした。自動露出のままでは LED の灯火が白飛びして色が取れません。観測中だけ露出を短く固定し、それ以外は自動に戻す制御を入れて、赤・青を正しく取れるようになりました。
7.2. 左右確認
真横向きの左右カメラで車両を検出し、検出枠が大きくなっていくかで接近を判定します。写っているだけの駐車車両や、遠ざかる車は接近としません。映像や推論結果が届いていない側は「不明」とし、「接近なし」と区別します。不明のままでは進みません。
コードの冒頭にも書いていますが、「接近なし」は安全の証明ではありません。現状は操作者の遠隔監視を前提にした補助機能です。
7.3. 踏切
警報灯の交互点滅(画像)と警報音(マイク、約2Hz の断続)を検出する機能を実装済みです。ただし実物の踏切では未確認で、既定では無効にしています。
8. 路面を知る: 歩道、縁石、草
歩道を走るロボットの最大の敵は、歩行者でも自転車でもなく、草でした。
8.1. 「歩道か車道か」は学習モデル任せにできなかった
路面のセマンティックセグメンテーション(YOLOX)で歩道と車道を見分け、迂回経路を歩道の中に留める、という構想でした。ところが実地で試すと、六角ブロック舗装の歩道がほぼ「車道」と判定されます。深度で見ても歩道と車道の段差は 2 cm 未満の場所があり、高さだけでも決まりません。
そこで方針を変えました。
- 事前情報を主にする: WP を結ぶ線は原則として歩道の上にある、とみなす。
- 縁石は観測で確かめる: 下向きカメラと前方カメラで、低い段差と縁石の影の線を検出し、「ここから先は車道側」という境界として迂回の計画に使う。
汎用モデルの出力をそのまま信じるのではなく、「このルートは人が歩道に引いた」という運用上の知識を一番強い手掛かりにした形です。
8.2. 草で止まる
ある日の走行では HOLD が89回。GCS フェイルセーフ(後述)を除くと、残りの大半は路肩の枯れ草、植え込み、縁石を障害物として検出したものでした。草は深度カメラには「路面から10 cm 出ている物体」として映ります。
対策は段階的に入れています。
- 植物セグメンテーション: 現場の画像で LRASPP-MobileNetV3 を学習し(植物の IoU 0.70)、「植物で、かつ低い(十数 cm まで)」点だけを停止判定から除外する。過去の停止ログを再生し、コーン・木のブロック・ポールを一度も植物と判定しないことを確認してから採用。
- 色や汎用モデルでの草判定は不採用: オフライン再生で、枯れ草は「路面」、ブロックは「植物」と出て使えなかった。
- ルートの横ずらし: 走行中に見えた左右の物までの距離から、向かっている WP を最大 0.3 m だけ反対側へずらして Cube のミッションに書き込む。草際ぎりぎりに置かれた WP を、走りながら少し内側へ寄せる。
横ずらしにも落とし穴がありました。ArduRover は走行中にミッション項目を書き換えると、その区間を最初からやり直します。書き込みのたびに減速し、ある走行では 258回の書き込みで速度が 0.1 m/s 近くまで落ちました。書き込み回数を絞る修正を入れています。
8.3. 点字ブロック
点字ブロックを避ける機能も作りましたが、黄色の検出は植え込みや枯れ草を誤検出し、実際の点字ブロックは色褪せたベージュでした。現在は無効にして作り直し中です。
9. 自己位置推定と GPS 喪失への備え
通常の位置と方位は、Cube の EKF が RTK-GNSS と2アンテナ方位から求めます。問題は、建物の陰や歩道橋の下で GNSS が劣化したときです。
localization ノードは、CAN から得る車輪速と Cube のジャイロで常時推測航法を走らせ、その結果を MAVLink の ODOMETRY として 10Hz で Cube に送り続けます。Cube の EKF には位置ソースを2系統設定してあり、GNSS を失ったときだけ外部推定へ切り替えます。
設計上の要点は次のとおりです。
- 切り替えても走行モードと WP は変えない。Cube は同じミッションを、推定位置で走り続ける。
- 準備できていなければ切り替えない。外部推定が Cube の位置と 0.3 m・3度以内で一致している、などの条件を満たしているときだけ。
- 戻るときは慎重に。復帰直後の GNSS は飛びやすいので、5秒間じっとしていることを確かめ、推定位置を最大 0.2 m/s でゆっくり GNSS に寄せてから戻す。
- フィードバックの循環を避ける。方位は Cube の推定ヨーの差分ではなく、生の角速度を積算する。外部推定を使っている最中の Cube の位置を、独立した測定として使い回さない。
実測では、30 m の走行で推測航法の位置誤差は RMSE 0.18 m、190度の旋回での方位誤差は 0.7% でした。100 m 級のルートでは RMSE 1.3〜1.8 m 程度で、GNSS の短い途切れをつなぐ用途には使える、という評価です。GPS 喪失時の自動切替そのものは、実機未確認です。
10. 安全の多層化
10月12日、AUTO で走行中の機体がポールに接触しました。原因は、scout-fusion を再起動した17秒後に AUTO へ入れたことです。認識ノードはまだ立ち上がりの途中で、停止判定が動いていませんでした。Cube とブリッジは Jetson の認識が生きているかを知らないので、そのまま走ってしまったのです。
この事故から、「止める仕組みが動いていること」を走行の条件にするインターロックを入れました。
proximity_feedは停止判定を1周するたびに、「判定が動いた・センサー欠測なし」というパケットをブリッジへ送る。- ブリッジは、これが1秒届かなければ AUTO / GUIDED の指令をゼロにする。
- 認識ノードの起動中・異常終了・フリーズのどれでも、車体は動かない。MANUAL は影響を受けない。
現在の安全機構を層で整理するとこうなります。
上の層ほど賢く、下の層ほど単純です。賢い層が壊れたときに、単純な層が止める。単純な層は C++ の小さなプロセスで、依存を極力持たせていません。
これは無人運用を保証するものではありません。現在の運用は、操作者が遠隔映像で監視し、いつでも介入できることが前提です。
11. 実走行ログと再生検証
現場で起きたことを机の上で再現できなければ、直したかどうか分かりません。開発の中盤からは、機能の追加よりも記録と再生の仕組みに時間を使いました。
11.1. 記録
scout-field-recorder がアームからディスアームまでを1トリップとして記録します。
- 台帳(追記専用の JSONL。
trips.jsonlとholds.jsonl): トリップごと・HOLD ごとの要約。後から書き換えず、訂正は訂正レコードとして追記する。 - 軽量ログ: モード、WP、停止原因、迂回制御器の状態、GNSS の Fix/Float。
- バルク(NPZ): LiDAR スキャン、融合結果、計画に使った格子。
- 画像窓: HOLD の前後だけ、関係するカメラの画像を保存。
proximity_feed 側の stop_status には、どのセンサーのどの区画が停止の原因か、どの判定ルールが効いたか、迂回制御器がどの状態で何を待っているかを出しています。
11.2. 再生
判断ロジックが ROS 非依存なので、記録したスキャンと状態をそのまま関数に流し込めます。
典型的な1日はこうです。朝に走らせて HOLD が31回出る → ログから原因別に分類する(右端の枯れ草が余裕 0.10 m に入った、前方カメラの平面推定が20秒失敗した、WP 至近で待ち続けた…)→ 原因ごとに修正する → 過去の HOLD を再生して、止まるべきでない場面で止まらず、止まるべき場面(コーン)では止まることを確かめる → 午後に走らせる。
ただし再生の限界も学びました。再生は「その時のセンサー入力に対して、新しいロジックがどう判断するか」しか分かりません。修正で車体の動きが変われば、その先のセンサー入力も変わります。再生で全件通っても実機で失敗したことがあり、単体テストが通ったら長いオフライン検証を重ねるより早めに実機で確かめる、という運用に落ち着いています。
12. 遠隔監視と通知
- Mission Planner: VPN 越しに
mavlink-routerの TCP へ接続。ミッションの編集、モード操作、近接表示。 - 映像: 前方・左右・信号機用のカメラを1枚に並べた H.264(960×540、15fps、約 1Mbps)を RTP で操作 PC へ。細部より動きの滑らかさを優先した設定。
- 音声: 機体のマイクとスピーカーで双方向通話(Opus)。現場の人に声を掛けられる。
- 通知: HOLD などのイベントを Telegram へ通知。LLM エージェントが読み取り専用の経路でログを調べ、HOLD の原因を答える仕組みも併設。
映像配信は走行の判断に一切関わらせていません。止めても走行に影響しない、という分離を保っています。
13. 現場で学んだこと
「何もない」と「見えていない」を区別する。 レーダーの空フレーム、左右カメラの「不明」、センサー鮮度による HOLD。安全に関わる入力はすべて、この区別を型として持たせました。
止める仕組みが生きていることを、走る条件にする。 ポール接触の教訓です。「再起動後は準備完了を確認してから AUTO に入れる」という運用ルールだけでは守れません。機械に確認させます。
記憶は諸刃の剣。 カメラの視野から外れた障害物(LiDAR に映らないコーンなど)を覚えておく「カメラ記憶」は、隙間通過に必須でした。しかし記憶の座標系が GNSS のドリフトでずれると、記憶した障害物が動いて見えます。車輪オドメトリ基準に変えたところ、今度は記憶が消える不具合でコーンに接触し、元に戻しました。覚えるなら、どの座標系で・いつ忘れるかまで設計が要ります。
14. 現在地とこれから
執筆時点の状態を正直に整理します。
| 項目 | 状態 |
|---|---|
| WP 追従、障害物停止、待機、局所迂回、AUTO 復帰 | 実機で稼働 |
| ポール間などの狭い隙間(0.82〜0.9 m 級)の通過 | 実機で通過実績あり。安定性は改善中 |
| 一時停止 WP での信号・左右確認による発進 | 稼働。現地での評価を継続中 |
| 植物の停止除外、ルートの横ずらし | 稼働。調整中 |
| 健全性インターロック、センサー鮮度停止 | 稼働 |
| GPS 喪失時の外部推定への自動切替 | 実装済み、実機未確認 |
| 踏切の警報灯・警報音の検出 | 実装済み、実物では未確認 |
| 点字ブロックの回避 | 無効化して作り直し中 |
歩道は、車道よりもずっと「決まりのない」環境でした。白線も車線もなく、幅は場所ごとに違い、草は季節で伸び、ポールの間隔は現地で測るまで分かりません。走らせるたびに新しい止まり方を見せてくれるので、記録して、分類して、1つずつ潰していく。その地道な繰り返しが、今のところ一番確かな前進の方法です。
15. FAQ
- 走行前に3D点群地図を作る必要はありますか?
- ありません。事前に与えるのは、GNSSの緯度・経度で構成したルート情報(ウェイポイント列)です。走行中の周囲状況は、LiDAR、深度カメラ、ミリ波レーダー、超音波センサーで観測します。
- ルート上に人や障害物がいた場合はどうなりますか?
- 現地のセンサー情報に基づいて減速またはHOLDします。障害物が動かなければ、観測済みの空き領域から局所的な迂回経路を計画し、GUIDEDで回避したあとAUTOミッションへ戻ります。
- GNSSが使えなくなっても走行できますか?
- 車輪速とジャイロを使った外部推定は実装済みで、短時間のGNSS途絶をつなぐ用途を想定しています。ただし、GNSS喪失時の自動切り替えは実機未確認です。
- 完全な無人運用に対応していますか?
- 現時点では対応していません。操作者が遠隔映像で監視し、プロポやMission Plannerからいつでも介入できる運用を前提としています。


