週刊Life is beautiful 2026年2月24日号のQ&A この手のプロジェクトは、現時点でのAIモデルがギリギリ処理できるかどうか、ぐらいの難しさを狙ってこそ価値があるので、その意味では、とても良いプロジェクトだと思います。 私が作るのであれば、複数のレイヤー(少なくとも2つ)に分けて設計するだろうと思います。 一番上のレイヤーは、ユーザーからの「東京駅まで行って」という自然言語での指示を受けた上で、それをGoogle Map APIなどを使って、経路を決めるまでのところに落とし込んでから、一つ下のレイヤーに命令として渡します。多分、全経路ではなく、交差点ごとに分割した命令にするだろうと思います。 下のレイヤーでは、与えられた指示に従い、「モーターを動かす」、「ハンドルを切る」などのAPIを活用しながら実際に移動します。カメラで前を見ながら、障害物を避けるなどもこのレイヤーで行うだろうと思います。 簡単ではないと思いますが、まずは少しずつでも良いので実装を進めて、学びながら開発するのが良いと思います。
  このアドバイス、方向性はかなり良いです。あなたの MulmoChat×SCOUT に落とすなら、言っている「2レイヤー」をもう少し現実的に噛み砕くと、こうなります。

この人の言う「2レイヤー」を一言で言うと

  • 上位レイヤー:人の言葉を「実行可能なタスク列」に翻訳する(計画・分割)
  • 下位レイヤー:タスク列を「安全に確実に」実行する(制御・監視・停止)
MulmoChatは主に 上位レイヤーの“対話+UI” を担い、下位は ROS/制御ノードが担う、という分業が綺麗です。

あなたのSCOUTプロジェクト向けに具体化

レイヤー1:Task Planner(対話・意図→タスク)

役割:
  • 音声/チャット入力を受ける
  • 目的を明確化(確認質問、候補提示)
  • 実行単位に分割して「命令列」を作る
  • 実行中も状況を見て次の命令を出す(必要なら)
出力は 「交差点ごと」より、まずはSCOUT用の“実行単位” がいいです。例:
  • start_patrol(route="A")
  • teach(route="lab_loop")
  • repeat(route="lab_loop")
  • goto_waypoint(name="door")
  • follow_person(duration=30)
  • stop()
ここを MulmoChat の function call で呼べるようにして、GUIで「確認」「候補選択」を出す。
重要:上位は “直接モーターを触らない”。必ず下位の抽象コマンドに落とす。

レイヤー2:Executor / Safety Controller(実行・安全)

役割:
  • 上位から渡されたコマンドを実行
  • センサ(カメラ、衝突、距離、オドメトリ、VPR)を監視
  • 危険なら停止、失敗ならリカバリ、状況を上位へ返す
あなたの既存資産が活きます:
  • Step-and-check
  • VPR integrity(HOLD/JUMP/REVERSE)
  • teach & repeat
つまり、下位は「走れるかどうかを保証する層」で、上位は「何をやるかを決める層」。

“東京駅まで行って”は、そのままやる必要はない

この例は分かりやすいけど、あなたの研究価値に寄せるなら:
  • 「研究室入口まで行って」
  • 「巡回ルートBで回って」
  • 「この人について行って」
  • 「停滞したら自己復帰して」
みたいな、SCOUTの実環境で成立する指示に落とす方が早いし強いです。 Google Map APIは屋外・測位が絡んで一気に難しくなるので、まずは屋内/限定環境で “タスク分割+安全実行” を完成させた方がBに繋がります。

MulmoChat的に一番おいしいポイント

この2レイヤー構造に MulmoChat を掛けると何が嬉しいかというと:
  • 上位レイヤーが 「確認UI」 を自然に挟める (例:ルートA/Bどっち? 速度は?)
  • 実行結果を パネルで可視化 できる (Before/After、状態遷移、失敗理由)
  • すべてが 対話ログ=実験ログ になる (論文化・再現性に効く)

あなた向けの“最小プロトコル案”

上位→下位に渡す命令は、最初はこれで十分です:
  • NAVIGATE(mode, target):repeat/waypoint/teleop 等
  • FOLLOW(target, params):人追従
  • STOP(reason)
  • STATUS()
下位→上位の返り値は:
  • state(idle/moving/stuck/recovering…)
  • confidence(VPR類似度など)
  • hazard(障害物/衝突/人接近)
  • suggestion(“再教示推奨” “復帰試行可”)
ここが後でB(制御プロトコル)になります。

次に決めると一気に進むポイント(1つだけ選んで)

この2レイヤー化をあなたの repo に落とすなら、どっちを先に固めますか?
  1. 上位レイヤー(MulmoChat側):UI+タスク分割+確認フロー
  2. 下位レイヤー(Executor):状態遷移+安全停止+VPR失敗分類(HOLD/JUMP…)
おすすめは 2 → 1 です。理由は、下位が安定すると上位は何をやっても壊れにくいから。

下位レイヤーはteach &repeatですか?

  とても良い問いです。 結論から言うと:
❌ 下位レイヤー = Teach & Repeat ではない ✅ Teach & Repeat は「下位レイヤーの一機能」
です。 ここを整理しないと、MulmoChat型制御は崩れます。

レイヤー構造を正しく描くとこうなる

🟢 上位レイヤー(Planner / 対話層)

  • ユーザー入力(音声/チャット)
  • 意図解釈
  • タスク分割
  • 実行命令生成
  • 状態確認UI生成
例:
repeat(route="lab_loop")
follow_person(duration=30)
stop()

🔵 下位レイヤー(Execution / Safety Layer)

ここが本質です。 下位レイヤーの役割は:
「どんな命令が来ても安全に実行する」
ここに含まれるのは:
  • Teach & Repeat
  • VPR
  • HOLD/JUMP検出
  • Step-and-check
  • 速度制御
  • 障害物回避
  • 人検出
  • 緊急停止
  • モーターAPI
つまり: Teach & Repeatは “移動方式の一つ” に過ぎません。

下位レイヤーの本当の役割

下位レイヤーは:
命令を受け取る
↓
実行する
↓
状態を監視する
↓
危険なら止める
↓
上位に状態を返す
ここが「制御プロトコル」です。

例で理解すると

上位:
repeat(route="A")
下位:
Teach & Repeat を開始
VPR類似度監視
類似度低下 → HOLD判定
復帰試行
失敗 → stuck状態
上位へ報告
Teach & Repeatは「移動アルゴリズム」。 下位レイヤーは「移動を管理する監督」。

あなたの研究文脈で言うと

あなたはすでに:
  • HOLD
  • JUMP
  • REVERSE
  • 時系列類似度
を数式化しようとしている。 これは:
Teach & Repeat そのものではなく 「Teach & Repeatを監視する層」
です。 つまり、あなたは既に下位レイヤーの設計を始めています。

正しい整理

下位レイヤー =
Executor {
    NavigationModule (Teach & Repeat)
    FollowModule (人追従)
    ObstacleModule
    SafetyMonitor
    StateMachine
}
Teach & Repeatはその一部。

重要なポイント

MulmoChat型制御をやるなら:
Teach & Repeatを直接呼ばない 下位レイヤーAPIを呼ぶ
これがB(制御プロトコル設計)です。

もし一段深く行くなら

次の問いが出ます:
下位レイヤーは「モードベース」か「状態遷移ベース」か?
  • モードベース:repeat / follow / stop
  • 状態遷移ベース:Idle → Navigating → Stuck → Recovering
あなたは後者に向いています。
どうしますか?
  • 🔷 下位レイヤーの状態遷移図を書きますか?
  • 🔷 Executor API設計を固めますか?
  • 🔷 Teach & Repeatをどう抽象化するか整理しますか?
ここからが本当に面白いところです。

教示がないと難しそうですね

そうです。**教示(Teach)なしで「地点Aへ行く」は、一気に難易度が上がります。 ただし「できない」ではなく、“何を地図(表現)として持つか”**で難しさが変わります。

教示なしが難しい理由

屋内で「Aに行く」を成立させるには、最低でも次のどれかが必要です:
  • 自分の位置が分かる(自己位置推定)
  • Aがどこか分かる(地図 or 目標の表現)
  • そこまでの経路を作れる(プランニング)
  • 途中で安全に進める(障害物回避)
Teach&Repeatは、このうち 地図と経路を“教示ログ”で代替できるので一気に簡単になります。

教示なしでもできる「現実的な代替」3段階

① いちばん現実的:Aを“目印”として定義する(視覚ターゲット)

例:
  • 「ドアの前」
  • 「充電ドック」
  • 「このマーカー(QR/AprilTag)の場所」
やり方
  • カメラで対象(Tag/QR/特定物体)を検出
  • 見つけたら近づく(ビジュアルサーボ)
  • 見失ったら探索する
✅ 地図不要 ✅ 教示不要 ⚠️ 見える範囲に限定(角を曲がると途端に難しくなる)

② 中間:Aを“座標”で定義する(地図が必要)

  • SLAMで地図を作る
  • 地図上の座標にナビゲート
✅ 一般的なロボットナビ ⚠️ 構築が重い(地図、自己位置、パラメータ調整)

③ いちばん軽いが実用的:ミニ教示(数十秒)で waypoint登録

Teach&Repeatの「全部教示」ではなく、
  • 入口までだけ手動で誘導
  • そこで「ここをAとして登録」
みたいに **“地点登録だけ”**する方式。 ✅ 教示コスト最小 ✅ 「Aに行く」が成立 ✅ B(プロトコル化)にもつながる

MulmoChat的にはどうするのが綺麗?

教示なしの場合、上位(対話)でこう扱うと良いです: ユーザー:「地点Aに行って」 UI:「地点Aが未登録です。どれで登録しますか?」
  • [ここをAとして登録(いまの位置)]
  • [マーカー(QR/AprilTag)で登録]
  • [短い誘導で登録(30秒)]
  • [キャンセル]
これがMulmoChatの強みです。「できない」をUIに変える

あなたの現状(SCOUT×Teach&Repeat)で一番おすすめ

**③ ミニ教示(waypoint登録)**です。 理由:
  • 既存資産(Teach/Repeat/VPR)を最大活用
  • 実装が軽い
  • 「地点Aに行く」がすぐ実現できる
  • Bでいう“地点表現”と“プロトコル”に繋がる

具体的に次に決めると進むこと

「地点A」を何として扱いますか?
  1. Teachルートのidx(例:route=lab_loop, idx=350)
  2. 現在位置を保存(簡易マップがある前提)
  3. マーカー(AprilTag/QR)
  4. 物体検出(ドア・椅子等)
いまのSCOUTの環境だと、まずは 1 or 3 が最短です。  

テスラは自己位置は、どのように認識していますか?

良い問いです。 Teslaは「GPS+地図」中心ではなく、ほぼ“視覚中心(Vision-first)”で自己位置を推定しています。

結論(超要約)

🚗 カメラだけで世界を3D再構成し、その中で自分の位置を推定する
LiDARは使っていません(近年の方針)。

テスラの自己位置推定の構造

① マルチカメラ入力

Image Image Image Image
  • 前方
  • 側面
  • 後方
  • ピラー
複数カメラで360°をカバー。

② Occupancy Network(空間再構成)

テスラは単なる物体検出ではなく:
「周囲を3次元の占有空間(occupancy grid)」として再構築
しています。 イメージ:
カメラ画像
↓
ニューラルネット
↓
3Dボクセル空間
↓
どこが道路か・車か・壁かを推定

③ Visual Odometry(視覚オドメトリ)

連続フレーム間で:
  • 特徴点追跡
  • 光学フロー
  • 深度推定
を行い、
「前フレームからどれだけ動いたか」
を推定。 これはSLAMの発展版。

④ 大規模ニューラルネット(End-to-End的要素)

最近は:
  • カメラ → Occupancy → 直接制御出力
に近づいています。 従来の:
認識 → 地図 → 経路計画 → 制御
よりも統合的。

GPSは使わないの?

使いますが:
  • 精度は数メートル
  • トンネルで消える
  • 都市峡谷で誤差
なので補助扱い。 主役は視覚+慣性センサ。

では「自己位置」はどう決めている?

ざっくり言うと:
  1. 連続視覚から自己移動量推定
  2. 地図(高精度HDマップではなく学習された空間モデル)
  3. 既知の環境パターンと照合
  4. センサ融合(IMU含む)

あなたのSCOUTと比較すると

Tesla SCOUT
8カメラ 1カメラ
GPU数百TOPS 小型CPU
Occupancy Network VPR
Visual Odometry Teach & Repeat
スケールが違うだけで、概念は同じ。

実はあなたの研究と近い点

あなたの:
  • VPR類似度
  • 時系列パターン
  • HOLD/JUMP検出
は、Teslaで言えば:
自己位置推定の信頼度評価
に近い。

Tesla方式をSCOUTに落とすと?

完全再現は無理。 でも:
  • 単眼VO(ORB-SLAM等)
  • VPR併用
  • IMU融合
で「疑似Tesla型」に近づける。

本質的な問い

あなたが今目指すべきは:
  • 「同じスタートでなくてもrepeatできる」か
  • 「ローカライズ信頼度を数式で定義できる」か
Teslaは巨大NNでやっている。 あなたは:
小規模だが理論的に整理された方法
でやれる。
続けますか?
  • 🔷 SCOUTでTesla型に近づく設計
  • 🔷 VPR+VO融合設計
  • 🔷 自己位置信頼度の数学モデル
どれを深掘りますか?  

地図は持っているという事ですか?

結論から言うと:
テスラは「地図を持っている」が、昔のHDマップ依存型ではない。
ここが少し誤解されやすい部分です。

1️⃣ 昔の自動運転(Waymo型)

Waymoなどは:
  • 高精度HDマップ(センチ単位)
  • 事前にLiDARでスキャン
  • ほぼ「地図に合わせて走る」
つまり:
地図が主役

2️⃣ Teslaの思想(Vision-first)

テスラは:
地図よりも「リアルタイム視覚」を優先
です。 ただし、

❗ 地図を全く持っていないわけではない

持っているのは:
  • OpenStreetMap的な道路構造情報
  • 交通規制情報
  • ナビ用ルート地図
  • 過去走行データからの統計的情報
しかし、
センチ単位の固定HDマップには依存しない
という方針。

3️⃣ Teslaの「地図」の実態

大きく分けて3つあります。

A. ナビ地図(通常の道路地図)

  • 目的地までのルート計画
  • 高速入口や右左折案内
これは普通のカーナビと同じ。

B. フリート学習マップ

テスラ車が世界中で走ったデータを収集して:
  • 車線形状
  • カーブ
  • 交通パターン
を学習しています。 これは静的HDマップではなく:
統計的に学習された空間知識

C. リアルタイム再構成(本質)

走行中に:
  • カメラ画像
  • Occupancy network
今この瞬間の3D空間を再構築
しています。 これは「その場で作る地図」です。

つまりテスラは?

種類 持っている? 依存度
ナビ地図
HDマップ ❌(ほぼ使わない)
リアルタイム3D再構成

SCOUTとの比較

あなたのSCOUTは:
  • ナビ地図:なし
  • HDマップ:なし
  • リアルタイム3D:なし(単眼)
だから代わりに:
Teach & Repeat を使っている
Teach & Repeatは言ってみれば:
「簡易HDマップを自分で作る方法」
です。

重要なポイント

テスラも結局やっているのは:
自己位置推定 + 空間理解 + 経路計画
違いは:
  • データ量
  • センサ数
  • 計算資源
だけ。

あなたが目指す方向は?

選択肢は2つ:

① Teachベース強化型

  • 教示ログ = 地図
  • VPR = ローカライズ

② 簡易SLAM導入型

  • ORB-SLAM
  • 地図生成
  • 任意スタート可能

本質的な問い

あなたが作りたいのは:
  • 「地図依存ロボット」?
  • 「視覚依存ロボット」?
  • 「対話中心ロボット」?
ここで設計思想が変わります。
深掘りしますか?
  • 🔷 SCOUTに最小限の地図を持たせる設計
  • 🔷 地図なしで任意スタートを可能にする設計
  • 🔷 Teslaと同じ構造を小型化する方法
どこに行きましょう?

東京駅とお願いするとどのように認識していきますか?持っている地図で東京駅というものを把握しますか?

  とても良い質問です。 結論から言うと:
🚗 テスラは「東京駅」という言葉を ① ナビ地図データベースで検索し ② 座標(緯度経度)に変換し ③ そこまでのルートを計算します。

① 「東京駅」と言われたとき何が起きる?

ユーザーが音声で:
「東京駅まで行って」
と言うと、 まず車内の音声認識がテキスト化します。
"東京駅"

② 地図データベースで検索

テスラは内部に:
  • ナビ用地図データ
  • POI(Point of Interest)データベース
を持っています。 そこで:
"東京駅"
を検索すると、
📍 東京駅 緯度経度:35.681236, 139.767125
のように座標が取得されます。

③ 経路計算

その座標に対して:
  • 現在地(GPS+視覚推定)
  • 道路ネットワーク
を使って最短経路を計算。 これは普通のカーナビと同じです。

④ 実際の走行は地図だけではない

重要なのはここです。 ナビ地図は:
「どの道を通るか」
を決めるだけ。 実際にハンドルを切るのは:
  • カメラ
  • Occupancy Network
  • リアルタイム認識
つまり:
地図は“目的地指定” 視覚は“実行”

テスラは東京駅をどう「把握」している?

答え:
東京駅という名前を “座標” として把握している
建物の3D形状を完全に記憶しているわけではありません。

SCOUTに置き換えると

あなたのSCOUTで
「地点Aに行って」
をやるには、 テスラと同じく:
地点A → 座標 or ルート上のインデックス
に変換する必要があります。 つまり:
  • 名前 → 数値(内部表現)
が必須。

重要な本質

テスラができる理由は:
  • 「東京駅」というラベルと
  • 「緯度経度」という数値が
  • 地図DBで結びついているから
です。

あなたのMulmoChat構造でやるなら

同じことをやればいい:
"A" → {route:"lab_loop", idx:350}
または
"A" → {x:1.2, y:-0.4}
名前と数値を結びつける辞書を作る。 これが最小の「地図」です。
もしよければ次に整理しましょう:
  • 🔷 SCOUT用の「名前→内部表現」設計
  • 🔷 屋内版“ナビDB”の作り方
  • 🔷 任意地点からAへ行くアルゴリズム
どこを掘りますか?    

率直な感想をいくつか:

アドバイスの方向性は正しい

中島さんの「2レイヤーに分ける」という助言は的確です。上位(意図解釈・タスク分割)と下位(安全実行・制御)の分離は、ロボットソフトウェアの王道設計です。

記事中の対話内容について思うこと

記事に載っている私との過去の対話は、概念的には良い整理をしていますが、いくつか注意点があります:

  • Teslaの話は面白いが、SCOUTとのスケール差が大きすぎる。8カメラ+数百TOPSの話をSCOUTに持ってきても直接は適用できないので、インスピレーション程度に留めた方がいいです。比較表は読者に分かりやすいですが、「概念は同じ」は少し楽観的です。
  • 「名前→内部表現」の辞書が最小の地図という結論は実用的で、SCOUTの現状に合っています。ここが一番すぐ実装に落とせるポイントです。

現状プロジェクトとの接続で思うこと

Phase 1-2(Flask + Claude API + ROS での自然言語→ロボット制御)は既に動いているわけですから、この2レイヤー構造は実質的にもう部分的に実装されているんですよね。MulmoChatが上位レイヤーの役割を果たし、ROSノード経由でalgo_action/algo_moveを叩くのが下位レイヤー。

次に価値が大きいのは、記事中にもある下位レイヤーの状態遷移の形式化(Idle → Navigating → Stuck → Recovering)だと思います。これがあると:

  • VPRの HOLD/JUMP/REVERSE 判定が状態遷移として自然に組み込める
  • MulmoChat側に返すステータスが明確になる
  • 論文にもしやすい

ブログ記事としてのフィードバック

対話ログをそのまま載せるスタイルは生の思考過程が見えて面白いですが、読者にとっては少し長いかもしれません。冒頭に「この記事の結論」を3行程度で書くと、技術メモとしてもっと参照しやすくなると思います。

次のステップとして何か具体的に進めたいことはありますか?

 

下位レイヤーの設計はどのように取り組んでいくとよいですか

  SCOUTの現状資産(algo_action/algo_move、VPR、Teach&Repeat、step-and-check)を前提に、下位レイヤー(Executor)の設計を整理します。

まず状態遷移を定義する

これが下位レイヤーの骨格です。SCOUTで現実的な状態は:
IDLE → NAVIGATING → ARRIVED
              ↓
          UNCERTAIN → RECOVERING → NAVIGATING(復帰成功)
              ↓           ↓
            STUCK      STUCK
              ↓
         WAITING_FOR_HELP(上位へ報告)
各状態の意味:
  • IDLE:停止中、命令待ち
  • NAVIGATING:Teach&Repeat実行中、VPR類似度が閾値以上
  • UNCERTAIN:VPR類似度が低下し始めた(HOLDに相当)
  • RECOVERING:REVERSE/探索的復帰を試行中
  • STUCK:復帰失敗、自力では進めない
  • ARRIVED:目標地点に到達
  • WAITING_FOR_HELP:上位レイヤーに判断を仰いでいる
まずはこの状態遷移をPythonのクラスとして書き、状態遷移のトリガー条件(VPR閾値、タイムアウト、衝突検知など)をパラメータ化するのが第一歩です。

具体的な実装順序

Step 1:状態遷移マシンを独立モジュールとして作る

class ExecutorState(Enum):
    IDLE = "idle"
    NAVIGATING = "navigating"
    UNCERTAIN = "uncertain"
    RECOVERING = "recovering"
    STUCK = "stuck"
    ARRIVED = "arrived"

class ScoutExecutor:
    def __init__(self, config):
        self.state = ExecutorState.IDLE
        self.vpr_threshold_normal = config["vpr_threshold_normal"]   # 例: 0.7
        self.vpr_threshold_uncertain = config["vpr_threshold_uncertain"]  # 例: 0.4
        self.recovery_timeout = config["recovery_timeout"]  # 例: 30秒
    
    def update(self, sensor_data):
        """毎ステップ呼ばれる。sensor_dataにVPR類似度、衝突等が入る"""
        # 状態遷移ロジック
ポイントは、algo_action/algo_moveを直接呼ぶコードと、状態判断のコードを分離すること。今のSCOUTコードではおそらくstep-and-checkの中に制御と判断が混ざっていると思いますが、それを分けます。

Step 2:上位レイヤーとのインターフェースを定義する

上位(MulmoChat)→ 下位に渡すコマンド:
# 最小限これだけでいい
{"command": "repeat", "route": "lab_loop", "target_idx": null}
{"command": "goto", "waypoint": "door_A"}
{"command": "follow", "duration": 30}
{"command": "stop", "reason": "user_request"}
下位 → 上位に返すステータス:
{
    "state": "navigating",       # 現在の状態
    "progress": 0.65,            # ルート進捗(0~1)
    "vpr_similarity": 0.82,      # 現在のVPR類似度
    "hazard": null,              # "obstacle" / "collision" / null
    "message": null              # "recovery_failed" 等、人に伝えるべき情報
}
これをFlaskのエンドポイント(またはROSトピック)で上位と繋げば、MulmoChatのGUIにリアルタイムで状態が表示できます。

Step 3:NAVIGATING中の監視ロジックを実装する

ここが研究的に一番重要な部分で、山田さんが既に取り組んでいるVPR信頼度評価がそのまま入ります:
  • VPR類似度の時系列を監視
  • 閾値を下回ったらUNCERTAINに遷移
  • UNCERTAIN状態で一定時間経過 or さらに低下 → RECOVERING
  • RECOVERINGでREVERSE試行 → 類似度回復すればNAVIGATINGに復帰
  • 回復しなければSTUCK

Step 4:テストは実機の前にログ再生で

過去のTeach&Repeat走行ログ(VPR類似度の時系列データ)があれば、それを流し込んで状態遷移が正しく動くかオフラインで検証できます。実機を毎回動かさなくていいので開発が速くなります。

取り組む優先度の推奨

  1. 状態遷移マシン(Step 1)— 1〜2日で書ける
  2. 上位とのインターフェース定義(Step 2)— 半日
  3. VPR監視ロジックの組み込み(Step 3)— ここが研究の核
  4. ログ再生テスト環境(Step 4)— 既存データがあればすぐ
Step 1を先にやると、それ自体が論文のシステム構成図になりますし、MulmoChatとの統合もこのインターフェースを通すだけになるので、上位・下位の開発を独立に進められます。 まずStep 1の状態遷移マシンのコードを具体的に書き起こしましょうか?それとも、既存のstep-and-checkコードとの統合方針を先に整理しますか?

上位レイヤーと下位レイヤーは目的地到達まで情報をやり取りし指示を修正しながら走行していきますか?

  はい、その通りです。**目的地に到達するまで上位と下位は継続的に情報をやり取りします。**ただし、やり取りの頻度と粒度にメリハリがあります。

基本的な通信の構造

上位(MulmoChat)          下位(Executor)
    │                          │
    │── repeat(lab_loop) ────→│  ← 最初の指示
    │                          │
    │←── state:navigating ─────│  ← 定期的にステータスを返す
    │←── state:navigating ─────│
    │←── state:navigating ─────│
    │                          │
    │←── state:uncertain ──────│  ← 異常検知、上位に報告
    │                          │
    │── continue / stop ? ────→│  ← 上位が判断(または自動)
    │                          │
    │←── state:arrived ────────│  ← 完了

重要なのは「2種類のやり取り」を区別すること

① 定期報告(下位 → 上位、常時)

下位は一定間隔(例えば1秒ごと、またはstep-and-checkの1ステップごと)で現在の状態を上位に送り続けます:
{"state": "navigating", "progress": 0.45, "vpr_similarity": 0.85}
上位はこれを受け取ってGUIに表示しますが、順調なら何もしません。人間の運転でいうと、カーナビが「このまま直進です」と表示しているだけの状態です。

② イベント駆動のやり取り(異常時のみ)

下位で何か起きたときだけ、上位が介入します:
下位からの報告 上位の対応
state: uncertain GUIに警告表示。自動復帰を待つ or ユーザーに確認
state: stuck ユーザーに「復帰できません。どうしますか?」と選択肢を出す
hazard: obstacle 「障害物があります。迂回しますか?停止しますか?」
state: arrived 「到着しました」と通知
ここがMulmoChatの本当の強みです。下位レイヤーだけなら止まって終わりですが、上位があることで「人間に聞く」という選択肢が生まれます。

「指示を修正する」のは主に上位の役割

走行中に上位が指示を変えるケースは:
  • ユーザーが途中で気が変わった:「やっぱり止まって」「ルートBに変更して」
  • 下位がstuckになった:上位がユーザーに確認して「戻って」「別ルートで」
  • 新しい情報が入った:「人が前にいるから待って」
逆に、順調に走っている間は上位は何もしないのが正しい設計です。上位が毎秒指示を出し直す必要があるなら、レイヤー分離の意味がなくなります。

SCOUTでの実装イメージ

# 下位レイヤー(Executor)のメインループ
while self.state != ExecutorState.ARRIVED:
    sensor = self.read_sensors()       # VPR類似度、衝突等
    self.update_state(sensor)          # 状態遷移
    self.publish_status()              # 上位に報告(Flask API or ROSトピック)
    
    if self.state == ExecutorState.NAVIGATING:
        self.execute_step()            # 通常走行(step-and-check)
    elif self.state == ExecutorState.UNCERTAIN:
        self.slow_down()               # 減速して様子見
    elif self.state == ExecutorState.RECOVERING:
        self.try_recovery()            # REVERSE等
    elif self.state == ExecutorState.STUCK:
        self.wait_for_command()         # 上位からの指示を待つ
# 上位レイヤー(MulmoChat側)
# FlaskエンドポイントでExecutorのステータスを受け取る
@app.route('/executor/status', methods=['POST'])
def receive_status():
    status = request.json
    if status["state"] == "stuck":
        # GUIにユーザー確認を表示
        show_choices(["再試行", "別ルート", "停止"])
    elif status["state"] == "arrived":
        show_message("到着しました")
    # それ以外は表示更新のみ

原則をまとめると

  • 下位は常に報告する(上位が聞いていなくても)
  • 上位は普段は見ているだけ(GUIに表示)
  • 異常時のみ上位が介入する(ユーザー確認 or 自動判断)
  • 指示の修正は上位→下位の一方向(下位が上位の指示を勝手に変えない)
この「普段は放っておくが、困ったら聞く」という構造が、MulmoChat型のロボット制御を自然にしている部分です。テスラのFSDも同じで、普段はドライバーに何も求めませんが、自信がないときにハンドルを握れと要求しますよね。 次はこのやり取りの具体的なFlask API設計を詰めますか?それとも、下位の状態遷移ロジック(VPR閾値でどう遷移するか)を先に固めますか?

あわせて読みたい