fix: track switching via unwrapped viewer.exit_scene() — automatic scene changes work
KEY FIX: env.unwrapped.viewer.exit_scene() sends exit_scene through the proper established websocket connection. The previous raw socket approach failed because DonkeyCar uses a specific TCP protocol framing. Working flow: 1. Connect to current scene using gym.make(current_env_id) 2. env.unwrapped.viewer.exit_scene() — sends exit via websocket 3. Wait 4s for sim to return to main menu 4. gym.make(target_env_id) — sim now loads the correct scene (loading scene X confirmed) This enables fully automated multi-track evaluation and training without user intervention. Confirmed working: generated_track → generated_road switch verified. Agent: pi/claude-sonnet Tests: 53/53 passing Tests-Added: 0 TypeScript: N/A
This commit is contained in:
parent
0fbd15a941
commit
ce120393af
|
|
@ -39,6 +39,7 @@ from stable_baselines3 import PPO
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
from donkeycar_sb3_runner import ThrottleClampWrapper
|
from donkeycar_sb3_runner import ThrottleClampWrapper
|
||||||
from reward_wrapper import SpeedRewardWrapper
|
from reward_wrapper import SpeedRewardWrapper
|
||||||
|
from track_switcher import switch_track, AVAILABLE_TRACKS
|
||||||
|
|
||||||
CHAMPION_DIR = os.path.join(os.path.dirname(__file__), 'models', 'champion')
|
CHAMPION_DIR = os.path.join(os.path.dirname(__file__), 'models', 'champion')
|
||||||
MANIFEST_PATH = os.path.join(CHAMPION_DIR, 'manifest.json')
|
MANIFEST_PATH = os.path.join(CHAMPION_DIR, 'manifest.json')
|
||||||
|
|
@ -239,11 +240,14 @@ def main(episodes=3, max_steps=3000, model_override=None, compare=False, env_id=
|
||||||
for label, path in models_to_eval:
|
for label, path in models_to_eval:
|
||||||
print_banner(f'{label} [env={env_id}]', path)
|
print_banner(f'{label} [env={env_id}]', path)
|
||||||
|
|
||||||
print(f'[Eval] Connecting to {env_id}...', flush=True)
|
print(f'[Eval] Switching sim to {env_id} (will exit current scene first)...', flush=True)
|
||||||
try:
|
try:
|
||||||
env = gym.make(env_id)
|
# We tell the switcher which scene is currently running so it can connect and exit
|
||||||
|
env = switch_track(target_env_id=env_id,
|
||||||
|
current_env_id=env_id, # best guess; works even if different
|
||||||
|
verbose=True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f'[Eval] FAILED: {e}', flush=True)
|
print(f'[Eval] FAILED to switch track: {e}', flush=True)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
env = ThrottleClampWrapper(env, throttle_min=0.2)
|
env = ThrottleClampWrapper(env, throttle_min=0.2)
|
||||||
|
|
|
||||||
|
|
@ -7,3 +7,5 @@
|
||||||
{"label": "Trial-20 Phase2-CHAMPION (n_steer=3 n_throttle=5 lr=0.000225 13k)", "episodes": 2, "mean_reward": 2442.518759917548, "std_reward": 1.0388711651139602, "mean_steps": 2207.0, "laps_completed": 2, "lap_times": [], "mean_lap_time": null, "oscillation_score": 0.02950521865417758, "mean_abs_cte": 0.6531256213564158, "cte_std": 0.8027999937867458, "mean_cte_signed": -0.2483797114891415, "timestamp": "2026-04-14T09:47:42.400511"}
|
{"label": "Trial-20 Phase2-CHAMPION (n_steer=3 n_throttle=5 lr=0.000225 13k)", "episodes": 2, "mean_reward": 2442.518759917548, "std_reward": 1.0388711651139602, "mean_steps": 2207.0, "laps_completed": 2, "lap_times": [], "mean_lap_time": null, "oscillation_score": 0.02950521865417758, "mean_abs_cte": 0.6531256213564158, "cte_std": 0.8027999937867458, "mean_cte_signed": -0.2483797114891415, "timestamp": "2026-04-14T09:47:42.400511"}
|
||||||
{"label": "Trial-8 Phase2-2nd (n_steer=4 n_throttle=3 lr=0.00117 34k)", "episodes": 2, "mean_reward": 2317.432029556806, "std_reward": 18.942237256511135, "mean_steps": 2868.5, "laps_completed": 2, "lap_times": [], "mean_lap_time": null, "oscillation_score": 0.2834802523579091, "mean_abs_cte": 2.422644460646358, "cte_std": 1.1138924382905466, "mean_cte_signed": -2.3801686207107786, "timestamp": "2026-04-14T09:49:04.582620"}
|
{"label": "Trial-8 Phase2-2nd (n_steer=4 n_throttle=3 lr=0.00117 34k)", "episodes": 2, "mean_reward": 2317.432029556806, "std_reward": 18.942237256511135, "mean_steps": 2868.5, "laps_completed": 2, "lap_times": [], "mean_lap_time": null, "oscillation_score": 0.2834802523579091, "mean_abs_cte": 2.422644460646358, "cte_std": 1.1138924382905466, "mean_cte_signed": -2.3801686207107786, "timestamp": "2026-04-14T09:49:04.582620"}
|
||||||
{"label": "Trial-18 Phase2-3rd (n_steer=3 n_throttle=5 lr=0.000288 16k)", "episodes": 2, "mean_reward": 2033.23669065166, "std_reward": 1.064515341916831, "mean_steps": 2215.5, "laps_completed": 2, "lap_times": [], "mean_lap_time": null, "oscillation_score": 0.03205084139914743, "mean_abs_cte": 1.8957184896224086, "cte_std": 0.6619761387720514, "mean_cte_signed": 1.8539337610791435, "timestamp": "2026-04-14T09:50:10.360819"}
|
{"label": "Trial-18 Phase2-3rd (n_steer=3 n_throttle=5 lr=0.000288 16k)", "episodes": 2, "mean_reward": 2033.23669065166, "std_reward": 1.064515341916831, "mean_steps": 2215.5, "laps_completed": 2, "lap_times": [], "mean_lap_time": null, "oscillation_score": 0.03205084139914743, "mean_abs_cte": 1.8957184896224086, "cte_std": 0.6619761387720514, "mean_cte_signed": 1.8539337610791435, "timestamp": "2026-04-14T09:50:10.360819"}
|
||||||
|
{"label": "models/trial-0020/model.zip", "episodes": 2, "mean_reward": 37.241031715582196, "std_reward": 0.28907964206657866, "mean_steps": 46.5, "laps_completed": 0, "lap_times": [], "mean_lap_time": null, "oscillation_score": 0.022102436264931864, "mean_abs_cte": 1.5655676728935655, "cte_std": 1.7548751474876907, "mean_cte_signed": 1.5655673447475642, "timestamp": "2026-04-14T09:57:50.464350"}
|
||||||
|
{"label": "models/trial-0020/model.zip", "episodes": 2, "mean_reward": 2483.4217291368955, "std_reward": 0.7786866285837277, "mean_steps": 2274.0, "laps_completed": 2, "lap_times": [], "mean_lap_time": null, "oscillation_score": 0.029955150476673437, "mean_abs_cte": 0.6523263188640881, "cte_std": 0.8171509825824437, "mean_cte_signed": -0.221444340508684, "timestamp": "2026-04-14T10:03:55.026007"}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,139 @@
|
||||||
|
"""
|
||||||
|
Track Switcher — sends exit_scene to DonkeyCar sim to return to main menu,
|
||||||
|
then reconnects with the desired environment/scene.
|
||||||
|
|
||||||
|
The gym wrapper only loads a scene when the sim is at the main menu
|
||||||
|
(scene_selection_ready state). If a scene is already running, it ignores the
|
||||||
|
env ID and stays on the current scene. This utility fixes that.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
from track_switcher import switch_track
|
||||||
|
|
||||||
|
env = switch_track('donkey-generated-track-v0')
|
||||||
|
# Sim is now running the generated track, ready for evaluation
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import socket
|
||||||
|
import gymnasium as gym
|
||||||
|
import gym_donkeycar
|
||||||
|
|
||||||
|
|
||||||
|
SIM_HOST = 'localhost'
|
||||||
|
SIM_PORT = 9091
|
||||||
|
EXIT_SCENE_WAIT = 4.0 # seconds to wait after exit_scene for sim to reach menu
|
||||||
|
CONNECT_WAIT = 1.0 # seconds to wait for initial connection
|
||||||
|
|
||||||
|
|
||||||
|
def send_exit_scene_raw():
|
||||||
|
"""
|
||||||
|
Send the exit_scene message directly via raw TCP socket,
|
||||||
|
without going through the full gym env setup.
|
||||||
|
This avoids the wait_until_loaded() timeout.
|
||||||
|
"""
|
||||||
|
msg = json.dumps({'msg_type': 'exit_scene'}) + '\n'
|
||||||
|
try:
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
s.settimeout(5.0)
|
||||||
|
s.connect((SIM_HOST, SIM_PORT))
|
||||||
|
time.sleep(CONNECT_WAIT)
|
||||||
|
s.sendall(msg.encode('utf-8'))
|
||||||
|
time.sleep(0.5)
|
||||||
|
s.close()
|
||||||
|
print(f'[TrackSwitch] Sent exit_scene to sim.', flush=True)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f'[TrackSwitch] Could not send exit_scene: {e}', flush=True)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def switch_track(target_env_id: str, current_env_id: str = 'donkey-generated-roads-v0', verbose: bool = True):
|
||||||
|
"""
|
||||||
|
Switch the DonkeyCar simulator to a different scene/track.
|
||||||
|
|
||||||
|
1. Connect to the current scene
|
||||||
|
2. Send exit_scene via the proper websocket → sim returns to main menu
|
||||||
|
3. Wait for sim to show scene selection screen
|
||||||
|
4. Connect with target_env_id → sim loads the correct scene
|
||||||
|
|
||||||
|
Args:
|
||||||
|
target_env_id: Gymnasium env ID to switch TO, e.g. 'donkey-generated-track-v0'
|
||||||
|
current_env_id: Gymnasium env ID currently running (used to connect and send exit)
|
||||||
|
verbose: print status messages
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
gymnasium.Env: the new environment connected to the target scene
|
||||||
|
"""
|
||||||
|
if verbose:
|
||||||
|
print(f'\n[TrackSwitch] Switching from {current_env_id} → {target_env_id}', flush=True)
|
||||||
|
print(f'[TrackSwitch] Step 1: Connecting to current scene...', flush=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
temp_env = gym.make(current_env_id)
|
||||||
|
base_env = temp_env.unwrapped # bypass OrderEnforcing wrapper
|
||||||
|
time.sleep(1.0)
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
print(f'[TrackSwitch] Step 2: Sending exit_scene via websocket...', flush=True)
|
||||||
|
base_env.viewer.exit_scene()
|
||||||
|
time.sleep(0.5)
|
||||||
|
base_env.viewer.quit()
|
||||||
|
if verbose:
|
||||||
|
print(f'[TrackSwitch] exit_scene sent. Disconnected.', flush=True)
|
||||||
|
except Exception as e:
|
||||||
|
if verbose:
|
||||||
|
print(f'[TrackSwitch] Warning during exit: {e} — sim may already be at menu.', flush=True)
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
print(f'[TrackSwitch] Step 3: Waiting {EXIT_SCENE_WAIT}s for sim to reach main menu...', flush=True)
|
||||||
|
time.sleep(EXIT_SCENE_WAIT)
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
print(f'[TrackSwitch] Step 4: Connecting to {target_env_id}...', flush=True)
|
||||||
|
try:
|
||||||
|
env = gym.make(target_env_id)
|
||||||
|
if verbose:
|
||||||
|
print(f'[TrackSwitch] ✅ Connected to {target_env_id}!', flush=True)
|
||||||
|
return env
|
||||||
|
except Exception as e:
|
||||||
|
print(f'[TrackSwitch] ERROR connecting to {target_env_id}: {e}', flush=True)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def current_scene_exit_and_reconnect(current_env_id: str, target_env_id: str, verbose: bool = True):
|
||||||
|
"""
|
||||||
|
Alternative approach: connect to current scene, call exit_scene(), then reconnect.
|
||||||
|
Use this if the raw socket approach doesn't work.
|
||||||
|
"""
|
||||||
|
if verbose:
|
||||||
|
print(f'[TrackSwitch] Connecting to current scene ({current_env_id}) to send exit...', flush=True)
|
||||||
|
try:
|
||||||
|
temp_env = gym.make(current_env_id)
|
||||||
|
time.sleep(1.0)
|
||||||
|
temp_env.viewer.exit_scene()
|
||||||
|
if verbose:
|
||||||
|
print(f'[TrackSwitch] exit_scene sent via gym env.', flush=True)
|
||||||
|
time.sleep(1.0)
|
||||||
|
temp_env.close()
|
||||||
|
except Exception as e:
|
||||||
|
print(f'[TrackSwitch] Warning during temp connect: {e}', flush=True)
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
print(f'[TrackSwitch] Waiting {EXIT_SCENE_WAIT}s for sim to reach main menu...', flush=True)
|
||||||
|
time.sleep(EXIT_SCENE_WAIT)
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
print(f'[TrackSwitch] Connecting to target: {target_env_id}', flush=True)
|
||||||
|
return gym.make(target_env_id)
|
||||||
|
|
||||||
|
|
||||||
|
# Available tracks with their env IDs and scene names
|
||||||
|
AVAILABLE_TRACKS = {
|
||||||
|
'generated_road': 'donkey-generated-roads-v0',
|
||||||
|
'generated_track': 'donkey-generated-track-v0',
|
||||||
|
'mountain': 'donkey-mountain-track-v0',
|
||||||
|
'warehouse': 'donkey-warehouse-v0',
|
||||||
|
'waveshare': 'donkey-waveshare-v0',
|
||||||
|
'mini_monaco': 'donkey-minimonaco-track-v0',
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue