はじめに
Windows 11 + WSL2(Ubuntu 26.04 LTS)上に、MuJoCoを使ったUnitree G1ヒューマノイドロボットのシミュレーション環境を構築した記録です。
- 学習済みRLポリシーで歩かせる「お手軽ルート」
- 実機と同じSDK(DDS通信)経由で自作の制御プログラムを動かす「開発向けルート」
- さらにキーボードで対話的に歩行方向を指示する
という3段階で構築しました。素直には進まず、いくつもの落とし穴にハマったので、解決策込みでまとめます。
環境
- OS: Windows 11 Home
- WSL2ディストリビューション: Ubuntu 26.04 LTS(システムPythonは3.14)
- GPU: 特になし(CPU推論のみ)
全体構成
| ルート | 使うリポジトリ | できること |
|---|---|---|
| ルートA | unitree_rl_gym | 学習済みポリシーで固定速度の歩行デモを再生 |
| ルートB | unitree_mujoco + unitree_sdk2_python | 実機と同じDDS経由でMuJoCo上のG1を制御 |
事前準備:Python 3.14問題への対応
Ubuntu 26.04 LTSはシステムPythonが3.14と新しく、cyclonedds(DDS通信ライブラリ)など一部パッケージが未対応でした。system Pythonを直接使わず、condaでPython 3.11の仮想環境を切っておくのが安全です。
mkdir -p ~/miniconda3
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O ~/miniconda3/miniconda.sh
bash ~/miniconda3/miniconda.sh -b -u -p ~/miniconda3
~/miniconda3/bin/conda init bash
source ~/.bashrc
初回、Anacondaのデフォルトチャンネルの利用規約同意を求められることがあります。
conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/main
conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/r
環境を作成します。
conda create -n unitree-sim python=3.11 -y
conda activate unitree-sim
ハマりポイント①:
conda activateを忘れて(base)(Python 3.14)のまま作業してしまい、原因不明のエラーに悩まされることが何度もありました。プロンプト先頭が(unitree-sim)になっているか、毎回確認するクセをつけるのがおすすめです。
ルートA:学習済みポリシーで歩行デモ
git clone https://github.com/unitreerobotics/unitree_rl_gym.git
cd unitree_rl_gym
setup.py が isaacgym(PyPIに存在しない、NVIDIAから個別入手が必要なパッケージ)を必須依存にしているため、素直に pip install -e . すると失敗します。依存解決をスキップして個別インストールします。
pip install -e . --no-deps
pip install torch mujoco pyyaml matplotlib numpy
ハマりポイント②:READMEどおり
mujoco==3.2.3と厳密にバージョン指定すると、環境によってはプリビルドwheelが無くソースビルドに失敗します(過去にWSLで経験したCMake/GLFWのビルドエラーと同種の問題)。バージョン指定を外し、使っているPythonに対応した最新版を入れるだけで解決しました。
インストール後、以下のような警告が出ますが無視してOKです(isaacgym等は学習専用で、再生には不要なため)。
ERROR: pip's dependency resolver does not currently take into account all the packages that are installed...
unitree-rl-gym 1.0.0 requires isaacgym, which is not installed.
実行:
python deploy/deploy_mujoco/deploy_mujoco.py g1.yaml
これでMuJoCoのウィンドウが開き、G1が学習済みポリシーで歩行するデモを確認できました。
※このデモはキーボード操作等の対話機能は無く、YAML内の cmd_init で固定された速度で直進するだけの再生専用スクリプトです。
ルートB:SDK経由でG1を制御する
実機と同じDDS通信インターフェースでG1を動かす、開発向けのルートです。
CycloneDDSライブラリの導入
unitree_sdk2_python は cyclonedds のPythonバインディングに依存していますが、これは裏でC言語ライブラリを必要とします。まず先にビルド・インストールします。
sudo apt update
sudo apt install -y cmake build-essential
cd ~
git clone https://github.com/eclipse-cyclonedds/cyclonedds -b releases/0.10.x
cd cyclonedds && mkdir build install && cd build
cmake .. -DCMAKE_INSTALL_PREFIX=../install
cmake --build . --target install
ハマりポイント③:この手順を踏まずに
pip3 install -e .するとCould not locate cyclonedds. Try to set CYCLONEDDS_HOME or CMAKE_PREFIX_PATHというエラーになります。
unitree_sdk2_pythonのインストール
conda activate unitree-sim
cd ~
git clone https://github.com/unitreerobotics/unitree_sdk2_python.git
cd unitree_sdk2_python
export CYCLONEDDS_HOME=~/cyclonedds/install
pip3 install -e .
pip3 install mujoco pygame
CYCLONEDDS_HOME はシェルセッションごとに消えるので、毎回設定するのが面倒であれば ~/.bashrc に追記しておくと良いです。
echo 'export CYCLONEDDS_HOME=~/cyclonedds/install' >> ~/.bashrc
unitree_mujocoのセットアップ
cd ~
git clone https://github.com/unitreerobotics/unitree_mujoco.git
unitree_mujoco/simulate_python/config.py を編集します。
ROBOT = "g1"
DOMAIN_ID = 0 # 実機側サンプルスクリプトがdomain 0固定のため合わせる
INTERFACE = "lo" # ループバック
USE_JOYSTICK = 0 # ★重要。詳細は後述
ハマりポイント④(最重要):
DOMAIN_IDはデフォルトで1(実機との混線防止用)ですが、SDK付属の多くのサンプルスクリプトはChannelFactoryInitialize(0, ...)のように domain 0固定 で書かれています。ここが食い違うと、シミュレータとクライアントが別のDDSドメインに存在することになり、エラーも出ずに何も反応しなくなります。実機を混在させないならDOMAIN_ID = 0に揃えるのが簡単です。
シミュレータ起動:
cd ~/unitree_mujoco/simulate_python
python3 ./unitree_mujoco.py
低レベル制御サンプルを動かす
unitree_sdk2_python/example/g1/ には high_level/(歩く・手を振る等の高レベルコマンド)と low_level/(関節PD制御)のサンプルがあります。
high_levelは動かない
high_level/g1_loco_client_example.py でコマンドを送ると、以下のようなエラーになります。
[ClientStub] send request error. id: 5493541235335
ハマりポイント⑤:
unitree_mujocoのsimulate_pythonは低レベル制御のみ対応(README にも明記)です。高レベルコマンド(歩行制御・WaveHand等)は実機の内蔵ファームウェアが処理しているもので、単純な物理シミュレータには実装されていません。high_level系のサンプルはこの環境では使えないと割り切って、low_levelを使います。
low_levelサンプルの実行時エラー
cd ~/unitree_sdk2_python/example/g1/low_level
python3 g1_low_level_example.py lo
これも最初は以下でクラッシュしました。
TypeError: 'NoneType' object is not subscriptable
Init() 内の MotionSwitcherClient(実機で高レベル制御を止めるための処理)が、シミュレータには存在しないサービスに問い合わせて失敗するのが原因でした。該当箇所をコメントアウトして回避します。
def Init(self):
# self.msc = MotionSwitcherClient()
# self.msc.SetTimeout(5.0)
# self.msc.Init()
# status, result = self.msc.CheckMode()
# while result['name']:
# self.msc.ReleaseMode()
# status, result = self.msc.CheckMode()
# time.sleep(1)
# create publisher #
ロボットが動かない → USE_JOYSTICKが原因
修正後に実行してもロボットが動かず、IMUの値も [0.0, 0.0, 0.0] のまま変化しませんでした。
ハマりポイント⑥:
config.pyのUSE_JOYSTICK = 1(デフォルト)が原因でした。物理コントローラーが接続されていないと、内部でsys.exit()が呼ばれ、物理演算スレッドが起動直後に停止します。他のDDS配信スレッドは動き続けるため、「エラーは出ないがデータも関節も一切動かない」という分かりにくい症状になります。USE_JOYSTICK = 0に変更し、シミュレータを再起動したところ解決しました。
立たせると倒れる → 仕様通り
USE_JOYSTICK を直すと今度は正しく物理演算が動き出し、G1が脱力して倒れました。
これは異常ではなく仕様です。
g1_low_level_example.pyはバランス制御を持たない「関節を直接動かすだけの検証用スクリプト」のため、重力に対して自立できません。倒れずに動きを見たい場合はconfig.pyのENABLE_ELASTIC_BAND = Trueにすると、胴体が仮想バネで宙に吊られた状態になり、関節の動きだけを確認できます(MuJoCoウィンドウ内で9キーがON/OFF、7/8キーで吊り高さ調整)。
キーボードでの対話操作(歩行方向の指示)
物理ゲームパッドをWSL2で使おうとした際にもう一つ大きな壁がありました。
USB接続コントローラーの壁
usbipd-win でWindows→WSL2へUSBデバイスを転送すること自体は成功しましたが(lsusb にコントローラーが表示される)、/dev/input/ ディレクトリごと存在せず、pygame からは検出できませんでした。
ハマりポイント⑦:WSL2の標準カーネルには、USB HID/ジョイスティック入力に必要なドライバ(
CONFIG_HID,CONFIG_INPUT_JOYDEV等)が組み込まれていません。対応するにはWSL2カーネルを自前でビルドし直す必要があり、手間が大きいため今回は見送りました。
キーボード入力への切り替え
代わりに、実機用の歩行制御スクリプト unitree_rl_gym/deploy/deploy_real/deploy_real.py(ジョイスティック入力でRLポリシーに速度指令を送る仕組みを持つ)を改造し、コントローラー入力部分をキーボード入力に置き換えました。
unitree_rl_gym/deploy/deploy_real/deploy_keyboard.py として新規作成:
from legged_gym import LEGGED_GYM_ROOT_DIR
from typing import Union
import numpy as np
import time
import torch
import sys
import termios
import tty
import select
import threading
from unitree_sdk2py.core.channel import ChannelPublisher, ChannelFactoryInitialize
from unitree_sdk2py.core.channel import ChannelSubscriber, ChannelFactoryInitialize
from unitree_sdk2py.idl.default import unitree_hg_msg_dds__LowCmd_, unitree_hg_msg_dds__LowState_
from unitree_sdk2py.idl.default import unitree_go_msg_dds__LowCmd_, unitree_go_msg_dds__LowState_
from unitree_sdk2py.idl.unitree_hg.msg.dds_ import LowCmd_ as LowCmdHG
from unitree_sdk2py.idl.unitree_go.msg.dds_ import LowCmd_ as LowCmdGo
from unitree_sdk2py.idl.unitree_hg.msg.dds_ import LowState_ as LowStateHG
from unitree_sdk2py.idl.unitree_go.msg.dds_ import LowState_ as LowStateGo
from unitree_sdk2py.utils.crc import CRC
from common.command_helper import create_damping_cmd, create_zero_cmd, init_cmd_hg, init_cmd_go, MotorMode
from common.rotation_helper import get_gravity_orientation, transform_imu_data
from common.remote_controller import KeyMap
from config import Config
class KeyboardController:
"""RemoteController互換のキーボード入力版。
w/s: 前進/後退, a/d: 左右移動, q/e: 左右旋回, space: 停止
g: start相当(脱力解除), f: A相当(制御開始), x: select相当(終了)
"""
def __init__(self):
self.lx = 0.0
self.ly = 0.0
self.rx = 0.0
self.ry = 0.0
self.button = [0] * 16
self._running = True
self._fd = sys.stdin.fileno()
self._old_settings = termios.tcgetattr(self._fd)
tty.setcbreak(self._fd)
self._thread = threading.Thread(target=self._loop, daemon=True)
self._thread.start()
print("[Keyboard] w/s:前後 a/d:左右 q/e:旋回 space:停止 g:start f:A x:select(終了)")
def _loop(self):
while self._running:
if select.select([sys.stdin], [], [], 0.05)[0]:
ch = sys.stdin.read(1)
if ch == "w":
self.ly = 1.0
elif ch == "s":
self.ly = -1.0
elif ch == "a":
self.lx = 1.0
elif ch == "d":
self.lx = -1.0
elif ch == "q":
self.rx = 1.0
elif ch == "e":
self.rx = -1.0
elif ch == " ":
self.lx = self.ly = self.rx = 0.0
elif ch == "g":
self.button[KeyMap.start] = 1
elif ch == "f":
self.button[KeyMap.A] = 1
elif ch == "x":
self.button[KeyMap.select] = 1
def restore(self):
self._running = False
termios.tcsetattr(self._fd, termios.TCSADRAIN, self._old_settings)
class Controller:
def __init__(self, config: Config) -> None:
self.config = config
self.remote_controller = KeyboardController()
self.policy = torch.jit.load(config.policy_path)
self.qj = np.zeros(config.num_actions, dtype=np.float32)
self.dqj = np.zeros(config.num_actions, dtype=np.float32)
self.action = np.zeros(config.num_actions, dtype=np.float32)
self.target_dof_pos = config.default_angles.copy()
self.obs = np.zeros(config.num_obs, dtype=np.float32)
self.cmd = np.array([0.0, 0, 0])
self.counter = 0
self._got_state = False
if config.msg_type == "hg":
self.low_cmd = unitree_hg_msg_dds__LowCmd_()
self.low_state = unitree_hg_msg_dds__LowState_()
self.mode_pr_ = MotorMode.PR
self.mode_machine_ = 0
self.lowcmd_publisher_ = ChannelPublisher(config.lowcmd_topic, LowCmdHG)
self.lowcmd_publisher_.Init()
self.lowstate_subscriber = ChannelSubscriber(config.lowstate_topic, LowStateHG)
self.lowstate_subscriber.Init(self.LowStateHgHandler, 10)
elif config.msg_type == "go":
self.low_cmd = unitree_go_msg_dds__LowCmd_()
self.low_state = unitree_go_msg_dds__LowState_()
self.lowcmd_publisher_ = ChannelPublisher(config.lowcmd_topic, LowCmdGo)
self.lowcmd_publisher_.Init()
self.lowstate_subscriber = ChannelSubscriber(config.lowstate_topic, LowStateGo)
self.lowstate_subscriber.Init(self.LowStateGoHandler, 10)
else:
raise ValueError("Invalid msg_type")
self.wait_for_low_state()
if config.msg_type == "hg":
init_cmd_hg(self.low_cmd, self.mode_machine_, self.mode_pr_)
elif config.msg_type == "go":
init_cmd_go(self.low_cmd, weak_motor=self.config.weak_motor)
def LowStateHgHandler(self, msg: LowStateHG):
self.low_state = msg
self.mode_machine_ = self.low_state.mode_machine
self._got_state = True
def LowStateGoHandler(self, msg: LowStateGo):
self.low_state = msg
self._got_state = True
def send_cmd(self, cmd: Union[LowCmdGo, LowCmdHG]):
cmd.crc = CRC().Crc(cmd)
self.lowcmd_publisher_.Write(cmd)
def wait_for_low_state(self):
while not self._got_state:
time.sleep(self.config.control_dt)
print("Successfully connected to the simulator.")
def zero_torque_state(self):
print("Enter zero torque state.")
print("Press 'g' to continue...")
while self.remote_controller.button[KeyMap.start] != 1:
create_zero_cmd(self.low_cmd)
self.send_cmd(self.low_cmd)
time.sleep(self.config.control_dt)
def move_to_default_pos(self):
print("Moving to default pos.")
total_time = 2
num_step = int(total_time / self.config.control_dt)
dof_idx = self.config.leg_joint2motor_idx + self.config.arm_waist_joint2motor_idx
kps = self.config.kps + self.config.arm_waist_kps
kds = self.config.kds + self.config.arm_waist_kds
default_pos = np.concatenate((self.config.default_angles, self.config.arm_waist_target), axis=0)
dof_size = len(dof_idx)
init_dof_pos = np.zeros(dof_size, dtype=np.float32)
for i in range(dof_size):
init_dof_pos[i] = self.low_state.motor_state[dof_idx[i]].q
for i in range(num_step):
alpha = i / num_step
for j in range(dof_size):
motor_idx = dof_idx[j]
target_pos = default_pos[j]
self.low_cmd.motor_cmd[motor_idx].q = init_dof_pos[j] * (1 - alpha) + target_pos * alpha
self.low_cmd.motor_cmd[motor_idx].qd = 0
self.low_cmd.motor_cmd[motor_idx].kp = kps[j]
self.low_cmd.motor_cmd[motor_idx].kd = kds[j]
self.low_cmd.motor_cmd[motor_idx].tau = 0
self.send_cmd(self.low_cmd)
time.sleep(self.config.control_dt)
def default_pos_state(self):
print("Enter default pos state.")
print("Press 'f' to start walking...")
while self.remote_controller.button[KeyMap.A] != 1:
for i in range(len(self.config.leg_joint2motor_idx)):
motor_idx = self.config.leg_joint2motor_idx[i]
self.low_cmd.motor_cmd[motor_idx].q = self.config.default_angles[i]
self.low_cmd.motor_cmd[motor_idx].qd = 0
self.low_cmd.motor_cmd[motor_idx].kp = self.config.kps[i]
self.low_cmd.motor_cmd[motor_idx].kd = self.config.kds[i]
self.low_cmd.motor_cmd[motor_idx].tau = 0
for i in range(len(self.config.arm_waist_joint2motor_idx)):
motor_idx = self.config.arm_waist_joint2motor_idx[i]
self.low_cmd.motor_cmd[motor_idx].q = self.config.arm_waist_target[i]
self.low_cmd.motor_cmd[motor_idx].qd = 0
self.low_cmd.motor_cmd[motor_idx].kp = self.config.arm_waist_kps[i]
self.low_cmd.motor_cmd[motor_idx].kd = self.config.arm_waist_kds[i]
self.low_cmd.motor_cmd[motor_idx].tau = 0
self.send_cmd(self.low_cmd)
time.sleep(self.config.control_dt)
def run(self):
self.counter += 1
for i in range(len(self.config.leg_joint2motor_idx)):
self.qj[i] = self.low_state.motor_state[self.config.leg_joint2motor_idx[i]].q
self.dqj[i] = self.low_state.motor_state[self.config.leg_joint2motor_idx[i]].dq
quat = self.low_state.imu_state.quaternion
ang_vel = np.array([self.low_state.imu_state.gyroscope], dtype=np.float32)
if self.config.imu_type == "torso":
waist_yaw = self.low_state.motor_state[self.config.arm_waist_joint2motor_idx[0]].q
waist_yaw_omega = self.low_state.motor_state[self.config.arm_waist_joint2motor_idx[0]].dq
quat, ang_vel = transform_imu_data(waist_yaw=waist_yaw, waist_yaw_omega=waist_yaw_omega, imu_quat=quat, imu_omega=ang_vel)
gravity_orientation = get_gravity_orientation(quat)
qj_obs = self.qj.copy()
dqj_obs = self.dqj.copy()
qj_obs = (qj_obs - self.config.default_angles) * self.config.dof_pos_scale
dqj_obs = dqj_obs * self.config.dof_vel_scale
ang_vel = ang_vel * self.config.ang_vel_scale
period = 0.8
count = self.counter * self.config.control_dt
phase = count % period / period
sin_phase = np.sin(2 * np.pi * phase)
cos_phase = np.cos(2 * np.pi * phase)
self.cmd[0] = self.remote_controller.ly
self.cmd[1] = self.remote_controller.lx * -1
self.cmd[2] = self.remote_controller.rx * -1
num_actions = self.config.num_actions
self.obs[:3] = ang_vel
self.obs[3:6] = gravity_orientation
self.obs[6:9] = self.cmd * self.config.cmd_scale * self.config.max_cmd
self.obs[9 : 9 + num_actions] = qj_obs
self.obs[9 + num_actions : 9 + num_actions * 2] = dqj_obs
self.obs[9 + num_actions * 2 : 9 + num_actions * 3] = self.action
self.obs[9 + num_actions * 3] = sin_phase
self.obs[9 + num_actions * 3 + 1] = cos_phase
obs_tensor = torch.from_numpy(self.obs).unsqueeze(0)
self.action = self.policy(obs_tensor).detach().numpy().squeeze()
target_dof_pos = self.config.default_angles + self.action * self.config.action_scale
for i in range(len(self.config.leg_joint2motor_idx)):
motor_idx = self.config.leg_joint2motor_idx[i]
self.low_cmd.motor_cmd[motor_idx].q = target_dof_pos[i]
self.low_cmd.motor_cmd[motor_idx].qd = 0
self.low_cmd.motor_cmd[motor_idx].kp = self.config.kps[i]
self.low_cmd.motor_cmd[motor_idx].kd = self.config.kds[i]
self.low_cmd.motor_cmd[motor_idx].tau = 0
for i in range(len(self.config.arm_waist_joint2motor_idx)):
motor_idx = self.config.arm_waist_joint2motor_idx[i]
self.low_cmd.motor_cmd[motor_idx].q = self.config.arm_waist_target[i]
self.low_cmd.motor_cmd[motor_idx].qd = 0
self.low_cmd.motor_cmd[motor_idx].kp = self.config.arm_waist_kps[i]
self.low_cmd.motor_cmd[motor_idx].kd = self.config.arm_waist_kds[i]
self.low_cmd.motor_cmd[motor_idx].tau = 0
self.send_cmd(self.low_cmd)
time.sleep(self.config.control_dt)
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("net", type=str, help="network interface")
parser.add_argument("config", type=str, help="config file name in the configs folder", default="g1.yaml")
args = parser.parse_args()
config_path = f"{LEGGED_GYM_ROOT_DIR}/deploy/deploy_real/configs/{args.config}"
config = Config(config_path)
ChannelFactoryInitialize(0, args.net)
controller = Controller(config)
try:
controller.zero_torque_state()
controller.move_to_default_pos()
controller.default_pos_state()
while True:
controller.run()
if controller.remote_controller.button[KeyMap.select] == 1:
break
except KeyboardInterrupt:
pass
finally:
create_damping_cmd(controller.low_cmd)
controller.send_cmd(controller.low_cmd)
controller.remote_controller.restore()
print("Exit")
このファイルは、既存の unitree_rl_gym の deploy/deploy_real/ フォルダ内(common/, config.py, configs/g1.yaml が既にある場所)に置く必要があります。
依存関係の追加インストール
unitree_rl_gym は最初 (base) 環境(Route A用)に入れていたため、unitree-sim 環境には legged_gym パッケージや torch 等が入っておらず、以下を個別に追加しました。
conda activate unitree-sim
cd (unitree_rl_gymのクローン先)
pip install -e . --no-deps
pip install torch pyyaml scipy
tickフィールドのバグへの対応
最後にもう一つ、deploy_real.py オリジナルの wait_for_low_state() は実機のLowStateメッセージの tick(カウンタ)フィールドを見て接続確認していますが、unitree_mujoco 側のシミュレータブリッジはこの tick フィールドを一切設定していません。そのため永遠に接続待ちのまま止まってしまいます。上記コードのように、独自の _got_state フラグを使って「メッセージを1回でも受信したか」で判定するよう書き換えることで解決しました。
実行
conda activate unitree-sim
cd (unitree_rl_gymのクローン先)/deploy/deploy_real
python3 deploy_keyboard.py lo g1.yaml
荒ぶるロボット
接続は問題なく、完了しキーボード操作にも反応しているようでしたが
ただひたすらロボットを荒ぶる状態を見せられただけでした。
今回はここで限界。。次回はもうちょっと意図した動きをさせたいと思います。
まとめ:ハマりポイント一覧
| # | 症状 | 原因 | 対処 |
|---|---|---|---|
| ① | conda環境を作ったつもりが反映されない |
conda activate 忘れ |
プロンプトの (env名) を都度確認 |
| ② |
mujoco==3.2.3 のwheelビルドエラー |
Python 3.14向けwheel未提供 | バージョン固定を外す |
| ③ | Could not locate cyclonedds |
CycloneDDS Cライブラリ未導入 | ソースからビルドし CYCLONEDDS_HOME を設定 |
| ④ | コマンドを送っても無反応 |
DOMAIN_ID の不一致(sim:1 / sample:0) |
どちらかに統一(今回は0に統一) |
| ⑤ | [ClientStub] send request error |
high_level制御はシミュレータ非対応 | low_levelサンプルを使う |
| ⑥ | TypeError: 'NoneType' object is not subscriptable |
MotionSwitcherClient がsimに存在しないサービスを呼ぶ |
該当処理をコメントアウト |
| ⑦ | 何も動かない・IMUが変化しない |
USE_JOYSTICK=1 でコントローラー未接続時に物理演算スレッドが停止 |
USE_JOYSTICK=0 に変更 |
| ⑧ | 起立させると倒れる | low_levelサンプルにバランス制御が無い(仕様) |
ENABLE_ELASTIC_BAND=True で宙吊り検証 or RLポリシー(ルートA)を使う |
| ⑨ | USBゲームパッドが pygame から見えない |
WSL2標準カーネルにHID/ジョイスティックドライバが無い | カスタムカーネルビルド(今回は見送り、キーボード代替) |
| ⑩ |
Successfully connected が出ない |
シミュレータ側が LowState.tick を更新しない |
独自の受信フラグに置き換え |
所感
実機用に書かれたSDK・サンプル群を「純粋な物理シミュレータ」に接続する際は、実機側のファームウェアが暗黙に担っていた処理(高レベル制御、コントローラー安全装置との連携、状態カウンタの更新等)が抜け落ちていることに起因するハマりどころが多い、という印象でした。エラーメッセージが出ない「無反応系」のトラブルは、DDSの通信経路(ドメインID/トピック/受信判定ロジック)を疑うのが近道でした。


