歩道を走る自動配送ロボットを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

最初に決めたのは役割分担です。

1.1. 事前の3D点群地図を作らずに走る

このロボットの特徴は、走行前にLiDARなどで現地を計測し、3D点群地図を作成しておく必要がないことです。事前に与えるのは、GNSSの緯度・経度で構成したルート情報(ウェイポイント列)だけです。

Mission Planner上のウェイポイント経路と機体周辺のLiDAR距離表示

走行中はそのルートを基準にしながら、LiDAR、深度カメラ、ミリ波レーダー、超音波センサーで周囲を観測します。経路上に人や車両、草、ポールなどがあれば、その場で減速・停止・待機・局所迂回を判断します。つまり、固定された事前地図へ周囲を照合して走るのではなく、GNSSで進行ルートを把握し、現地の状況は搭載センサーで判断する構成です。

ROS 2 の Nav2 でグローバル経路から全部を組む構成も考えられますが、次の理由でこの形にしました。

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 LiDARRPLIDAR S2全周の障害物、局所経路の「空き」の根拠
深度カメラ(前方)RealSense(水平設置)前方の障害物、路面推定、信号機認識、路面セグメンテーション
深度カメラ(下向き)RealSense(俯角15度)足元の低い障害物、段差・穴、縁石
ミリ波レーダーT18PE(79GHz)前方の移動物の検出
超音波 ×4A02YYUW(左・右・左後・右後)至近距離の側方・後方
左右カメラ ×2USB カメラ(真横向き)交差点での接近車両の確認
信号機用カメラLogicool C270(前向き)一時停止地点での信号機・踏切警報灯。内蔵マイクは警報音と遠隔通話に利用
後方カメラU20CAM-1080p後退前の人・車の確認
外部 IMUTR-IMU推測航法の比較・検証用

USB の接続先は udev で /dev/sensors/lidar-s2 のような固定名にしています。FTDI 製の USB シリアルには個体シリアルがないものがあり、その場合は物理ポートの位置で識別しています。


3. ソフトウェア構成

3.1. プロセスの単位は systemd サービス

機体のソフトは systemd サービスとして常駐します。起動順は mavlink-router-scout → scout-cameras → scout-fusion です。

サービス内容
mavlink-router-scoutCube の UART を唯一開くプロセス。TCP で他へ中継
scout-bridgeC++ 製の駆動ブリッジ。Cube の PWM → CAN
scout-camerasRealSense 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 が言ったとおりに動かす」だけの薄い層ですが、止める方向の入力だけは別経路で受け付けます。その代表が次章以降で出てくる健全性インターロックです。


5. 障害物検知と停止判定

5.1. 72区画への融合

LiDAR、前方・下向きカメラ、レーダーの検出結果を、車体中心まわりの 5度刻み72区画 の距離に融合します。これは MAVLink の OBSTACLE_DISTANCE の形式そのままで、約10Hz で Cube にも送っています(Mission Planner の近接表示に出ます)。

ただし、止める判断は Cube の近接回避機能に任せず Jetson がします。Cube 側の回避は全周に一律のマージンを取るため、歩道では横の壁や植え込みで止まってしまうからです。

5.2. 止める領域

AUTO で走行中、次の領域に検出があれば HOLD を要求します。

横や後ろに物が「あるだけ」では止まりません。歩道は横に物があるのが普通だからです。

さらに、停止領域の手前 1.5 m に入った時点で AUTO の速度を 0.35 m/s まで落とします。巡航 0.75 m/s からいきなり止まるより、制動距離にも荷物にも優しいためです。

5.3. カメラ深度の扱い

深度カメラは「路面から何 cm 出ているか」を知るために、毎フレーム路面の平面を推定しています。ここが屋外では手強い部分でした。

5.4. センサーが黙ったら止まる

必須センサーの処理結果が設定した鮮度閾値より古くなったら、障害物がなくても HOLD します。「見えていない」と「何もない」を混同しないための基本ルールです。レーダーは検出ゼロのときも空フレームを配信させ、無検出と故障を区別しています。

LiDAR は USB の瞬断などで無応答になることがあり、ウォッチドッグノードがドライバを再起動します(試験では停止から19秒で自動復旧)。


6. 止まったあと: 待機・局所迂回・復帰

止まるだけなら簡単です。難しいのは「止まったあとどうするか」でした。

6.1. 待つ

HOLD 後、まず5秒待ちます。相手が歩行者なら、たいていその間にいなくなります。レーダーが前方の移動物(0.2 m/s 以上)を検出している間は、待機時間を過ぎても迂回を始めません。動いている人を避けに行くより、通り過ぎるのを待つ方が安全で、結果的に早いからです。

一方、止まった原因が路面の低い物だけ(LiDAR にもレーダーにも映らず、カメラの点がすべて低い)なら待つ意味がないので、待機を省いてすぐ計画に進みます。

6.2. 計画する: 「観測済みの空き」だけで迂回経路を探す

局所経路は、LiDAR が実際に測距して空いていると確認できた領域だけを使い、障害物を避ける経路を探索して引きます。未観測の場所は通りません。障害物の陰で合流点が見えないときは、見えている側方の中継点まで進んでから計画し直します。

車体モデルは2段構えです。

  1. まず円形(回転しても当たらない安全側)で外側を回る経路を探す。
  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. それでも詰まったら

無限に頑張らせない、というのも設計のうちです。


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 未満の場所があり、高さだけでも決まりません。

そこで方針を変えました。

汎用モデルの出力をそのまま信じるのではなく、「このルートは人が歩道に引いた」という運用上の知識を一番強い手掛かりにした形です。

8.2. 草で止まる

ある日の走行では HOLD が89回。GCS フェイルセーフ(後述)を除くと、残りの大半は路肩の枯れ草、植え込み、縁石を障害物として検出したものでした。草は深度カメラには「路面から10 cm 出ている物体」として映ります。

対策は段階的に入れています。

横ずらしにも落とし穴がありました。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 を失ったときだけ外部推定へ切り替えます。

設計上の要点は次のとおりです。

実測では、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 の認識が生きているかを知らないので、そのまま走ってしまったのです。

この事故から、「止める仕組みが動いていること」を走行の条件にするインターロックを入れました。

現在の安全機構を層で整理するとこうなります。

上の層ほど賢く、下の層ほど単純です。賢い層が壊れたときに、単純な層が止める。単純な層は C++ の小さなプロセスで、依存を極力持たせていません。

これは無人運用を保証するものではありません。現在の運用は、操作者が遠隔映像で監視し、いつでも介入できることが前提です。


11. 実走行ログと再生検証

現場で起きたことを机の上で再現できなければ、直したかどうか分かりません。開発の中盤からは、機能の追加よりも記録と再生の仕組みに時間を使いました。

11.1. 記録

scout-field-recorder がアームからディスアームまでを1トリップとして記録します。

proximity_feed 側の stop_status には、どのセンサーのどの区画が停止の原因か、どの判定ルールが効いたか、迂回制御器がどの状態で何を待っているかを出しています。

11.2. 再生

判断ロジックが ROS 非依存なので、記録したスキャンと状態をそのまま関数に流し込めます。

典型的な1日はこうです。朝に走らせて HOLD が31回出る → ログから原因別に分類する(右端の枯れ草が余裕 0.10 m に入った、前方カメラの平面推定が20秒失敗した、WP 至近で待ち続けた…)→ 原因ごとに修正する → 過去の HOLD を再生して、止まるべきでない場面で止まらず、止まるべき場面(コーン)では止まることを確かめる → 午後に走らせる。

ただし再生の限界も学びました。再生は「その時のセンサー入力に対して、新しいロジックがどう判断するか」しか分かりません。修正で車体の動きが変われば、その先のセンサー入力も変わります。再生で全件通っても実機で失敗したことがあり、単体テストが通ったら長いオフライン検証を重ねるより早めに実機で確かめる、という運用に落ち着いています。


12. 遠隔監視と通知

映像配信は走行の判断に一切関わらせていません。止めても走行に影響しない、という分離を保っています。


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からいつでも介入できる運用を前提としています。