diff --git a/tools/sim/README.md b/tools/sim/README.md index 3cc124cdbb..c77bd718ac 100644 --- a/tools/sim/README.md +++ b/tools/sim/README.md @@ -1,44 +1,56 @@ openpilot in simulator ===================== -openpilot implements a [bridge](bridge.py) that allows it to run in the [CARLA simulator](https://carla.org/). +openpilot implements a [bridge](bridge.py) that allows it to run in the [CARLA simulator](https://carla.org/). ## System Requirements -openpilot doesn't have any extreme hardware requirements, however CARLA requires an NVIDIA graphics card and is very resource-intensive and may not run smoothly on your system. For this case, we have a low quality mode you can activate by running: -``` -./start_openpilot_docker.sh --low_quality -``` +openpilot doesn't have any extreme hardware requirements, however CARLA requires an NVIDIA graphics card and is very resource-intensive and may not run smoothly on your system. +For this case, we have a the simulator in low quality by default. You can also check out the [CARLA python documentation](https://carla.readthedocs.io/en/latest/python_api/) to find more parameters to tune that might increase performance on your system. ## Running the simulator - -First, start the CARLA server in one terminal. -``` +Start Carla simulator, openpilot and bridge processes located in tools/sim: +``` bash +# Terminal 1 ./start_carla.sh + +# Terminal 2 - Run openpilot and bridge in one Docker: +./start_openpilot_docker.sh + +# Running the latest local code execute + # Terminal 2: + ./launch_openpilot.sh + # Terminal 3 + ./bridge.py ``` -Then, start the bridge and openpilot in another terminal. +### Bridge usage +_Same commands hold for start_openpilot_docker_ ``` -./start_openpilot_docker.sh +$ ./bridge.py -h +Usage: bridge.py [options] +Bridge between CARLA and openpilot. + +Options: + -h, --help show this help message and exit + --joystick Use joystick input to control the car + --high_quality Set simulator to higher quality (requires good GPU) + --town TOWN Select map to drive in + --spawn_point NUM Number of the spawn point to start in ``` To engage openpilot press 1 a few times while focused on bridge.py to increase the cruise speed. - -## Controls - -You can control openpilot driving in the simulation with the following keys - -| key | functionality | -| :---: | :---------------: | -| 1 | Cruise up 5 mph | -| 2 | Cruise down 5 mph | -| 3 | Cruise cancel | -| q | Exit all | - -To see the options for changing the environment, such as the town, spawn point or precipitation, you can run `./start_openpilot_docker.sh --help`. -This will print the help output inside the docker container. You need to exit the docker container before running `./start_openpilot_docker.sh` again. +All inputs: + +| key | functionality | +|:----:|:-----------------:| +| 1 | Cruise up 5 mph | +| 2 | Cruise down 5 mph | +| 3 | Cruise cancel | +| q | Exit all | +| wasd | Control manually | ## Further Reading diff --git a/tools/sim/bridge.py b/tools/sim/bridge.py index 0b07c1b0e5..368e18fd92 100755 --- a/tools/sim/bridge.py +++ b/tools/sim/bridge.py @@ -32,11 +32,10 @@ STEER_RATIO = 15. pm = messaging.PubMaster(['roadCameraState', 'wideRoadCameraState', 'sensorEvents', 'can', "gpsLocationExternal"]) sm = messaging.SubMaster(['carControl', 'controlsState']) - def parse_args(add_args=None): parser = argparse.ArgumentParser(description='Bridge between CARLA and openpilot.') parser.add_argument('--joystick', action='store_true') - parser.add_argument('--low_quality', action='store_true') + parser.add_argument('--high_quality', action='store_true') parser.add_argument('--town', type=str, default='Town04_Opt') parser.add_argument('--spawn_point', dest='num_selected_spawn_point', type=int, default=16) @@ -86,10 +85,9 @@ class Camerad: # TODO: move rgb_to_yuv.cl to local dir once the frame stream camera is removed kernel_fn = os.path.join(BASEDIR, "selfdrive", "camerad", "transforms", "rgb_to_yuv.cl") - self._kernel_file = open(kernel_fn) - - prg = cl.Program(self.ctx, self._kernel_file.read()).build(cl_arg) - self.krnl = prg.rgb_to_yuv + with open(kernel_fn) as f: + prg = cl.Program(self.ctx, f.read()).build(cl_arg) + self.krnl = prg.rgb_to_yuv self.Wdiv4 = W // 4 if (W % 4 == 0) else (W + (4 - W % 4)) // 4 self.Hdiv4 = H // 4 if (H % 4 == 0) else (H + (4 - H % 4)) // 4 @@ -130,10 +128,6 @@ class Camerad: setattr(dat, pub_type, msg) pm.send(pub_type, dat) - def close(self): - self._kernel_file.close() - - def imu_callback(imu, vehicle_state): vehicle_state.bearing_deg = math.degrees(imu.compass) dat = messaging.new_message('sensorEvents', 2) @@ -240,13 +234,13 @@ def can_function_runner(vs: VehicleState, exit_event: threading.Event): def connect_carla_client(): client = carla.Client("127.0.0.1", 2000) - client.set_timeout(10) + client.set_timeout(5) return client class CarlaBridge: - def __init__(self, args): + def __init__(self, arguments): set_params_enabled() msg = messaging.new_message('liveCalibration') @@ -254,32 +248,48 @@ class CarlaBridge: msg.liveCalibration.rpyCalib = [0.0, 0.0, 0.0] Params().put("CalibrationParams", msg.to_bytes()) - self._args = args + self._args = arguments self._carla_objects = [] self._camerad = None - self._threads_exit_event = threading.Event() + self._exit_event = threading.Event() self._threads = [] - self._shutdown = False + self._keep_alive = True self.started = False signal.signal(signal.SIGTERM, self._on_shutdown) + self._exit = threading.Event() def _on_shutdown(self, signal, frame): - self._shutdown = True + self._keep_alive = False def bridge_keep_alive(self, q: Queue, retries: int): - while not self._shutdown: - try: - self._run(q) - break - except RuntimeError as e: - if retries == 0: - raise - retries -= 1 - print(f"Restarting bridge. Retries left {retries}. Error: {e} ") + try: + while self._keep_alive: + try: + self._run(q) + break + except RuntimeError as e: + self.close() + if retries == 0: + raise + + # Reset for another try + self._carla_objects = [] + self._threads = [] + self._exit_event = threading.Event() + + retries -= 1 + if retries <= -1: + print(f"Restarting bridge. Error: {e} ") + else: + print(f"Restarting bridge. Retries left {retries}. Error: {e} ") + finally: + # Clean up resources in the opposite order they were created. + self.close() def _run(self, q: Queue): client = connect_carla_client() world = client.load_world(self._args.town) + settings = world.get_settings() settings.synchronous_mode = True # Enables synchronous mode settings.fixed_delta_seconds = 0.05 @@ -287,7 +297,7 @@ class CarlaBridge: world.set_weather(carla.WeatherParameters.ClearSunset) - if self._args.low_quality: + if not self._args.high_quality: world.unload_map_layer(carla.MapLayer.Foliage) world.unload_map_layer(carla.MapLayer.Buildings) world.unload_map_layer(carla.MapLayer.ParkedVehicles) @@ -324,7 +334,7 @@ class CarlaBridge: blueprint.set_attribute('image_size_x', str(W)) blueprint.set_attribute('image_size_y', str(H)) blueprint.set_attribute('fov', str(fov)) - if self._args.low_quality: + if not self._args.high_quality: blueprint.set_attribute('enable_postprocess_effects', 'False') camera = world.spawn_actor(blueprint, transform, attach_to=vehicle) camera.listen(callback) @@ -332,7 +342,7 @@ class CarlaBridge: self._camerad = Camerad() road_camera = create_camera(fov=40, callback=self._camerad.cam_callback_road) - road_wide_camera = create_camera(fov=163, callback=self._camerad.cam_callback_wide_road) # fov bigger than 163 shows unwanted artifacts + road_wide_camera = create_camera(fov=120, callback=self._camerad.cam_callback_wide_road) # fov bigger than 120 shows unwanted artifacts self._carla_objects.extend([road_camera, road_wide_camera]) @@ -349,16 +359,13 @@ class CarlaBridge: self._carla_objects.extend([imu, gps]) # launch fake car threads - self._threads.append(threading.Thread(target=panda_state_function, args=(vehicle_state, self._threads_exit_event,))) - self._threads.append(threading.Thread(target=peripheral_state_function, args=(self._threads_exit_event,))) - self._threads.append(threading.Thread(target=fake_driver_monitoring, args=(self._threads_exit_event,))) - self._threads.append(threading.Thread(target=can_function_runner, args=(vehicle_state, self._threads_exit_event,))) + self._threads.append(threading.Thread(target=panda_state_function, args=(vehicle_state, self._exit_event,))) + self._threads.append(threading.Thread(target=peripheral_state_function, args=(self._exit_event,))) + self._threads.append(threading.Thread(target=fake_driver_monitoring, args=(self._exit_event,))) + self._threads.append(threading.Thread(target=can_function_runner, args=(vehicle_state, self._exit_event,))) for t in self._threads: t.start() - # can loop - rk = Ratekeeper(100, print_delay_threshold=0.05) - # init throttle_ease_out_counter = REPEAT_COUNTER brake_ease_out_counter = REPEAT_COUNTER @@ -376,7 +383,14 @@ class CarlaBridge: brake_manual_multiplier = 0.7 # keyboard signal is always 1 steer_manual_multiplier = 45 * STEER_RATIO # keyboard signal is always 1 - while not self._shutdown: + # Simulation tends to be slow in the initial steps. This prevents lagging later + for _ in range(20): + world.tick() + + # loop + rk = Ratekeeper(100, print_delay_threshold=0.05) + + while self._keep_alive: # 1. Read the throttle, steer and brake from op or manual controls # 2. Set instructions in Carla # 3. Send current carstate to op via can @@ -495,13 +509,9 @@ class CarlaBridge: rk.keep_time() self.started = True - # Clean up resources in the opposite order they were created. - self.close() - def close(self): - self._threads_exit_event.set() - if self._camerad is not None: - self._camerad.close() + self.started = False + self._exit_event.set() for s in self._carla_objects: try: s.destroy() diff --git a/tools/sim/lib/keyboard_ctrl.py b/tools/sim/lib/keyboard_ctrl.py index 1c50b9b891..803aa091a3 100644 --- a/tools/sim/lib/keyboard_ctrl.py +++ b/tools/sim/lib/keyboard_ctrl.py @@ -38,7 +38,7 @@ def getch() -> str: def keyboard_poll_thread(q: 'Queue[str]'): while True: c = getch() - # print("got %s" % c) + print("got %s" % c) if c == '1': q.put("cruise_up") elif c == '2': diff --git a/tools/sim/start_carla.sh b/tools/sim/start_carla.sh index 8c3b140c82..912c7d6f08 100755 --- a/tools/sim/start_carla.sh +++ b/tools/sim/start_carla.sh @@ -25,4 +25,4 @@ docker run \ -v /tmp/.X11-unix:/tmp/.X11-unix:rw \ -it \ carlasim/carla:0.9.12 \ - /bin/bash ./CarlaUE4.sh -opengl -nosound -RenderOffScreen -benchmark -fps=20 -quality-level=High + /bin/bash ./CarlaUE4.sh -opengl -nosound -RenderOffScreen -benchmark -fps=20 -quality-level=Low diff --git a/tools/sim/test/test_carla_integration.py b/tools/sim/test/test_carla_integration.py index e70e881f0f..8f76abb561 100755 --- a/tools/sim/test/test_carla_integration.py +++ b/tools/sim/test/test_carla_integration.py @@ -15,27 +15,18 @@ class TestCarlaIntegration(unittest.TestCase): Tests need Carla simulator to run """ processes = None + carla_process = None def setUp(self): self.processes = [] - # We want to make sure that carla_sim docker is still running. Skip output shell + # We want to make sure that carla_sim docker isn't still running. subprocess.run("docker rm -f carla_sim", shell=True, stderr=subprocess.PIPE, check=False) - self.processes.append(subprocess.Popen(".././start_carla.sh")) + self.carla_process = subprocess.Popen(".././start_carla.sh") # Too many lagging messages in bridge.py can cause a crash. This prevents it. unblock_stdout() - - def test_run_bridge(self): - # Test bridge connect with carla and runs without any errors for 60 seconds - test_duration = 60 - - carla_bridge = CarlaBridge(bridge.parse_args(['--low_quality'])) - p = carla_bridge.run(Queue(), retries=3) - self.processes = [p] - - time.sleep(test_duration) - - self.assertEqual(p.exitcode, None, f"Bridge process should be running, but exited with code {p.exitcode}") + # Wait 10 seconds to startup carla + time.sleep(10) def test_engage(self): # Startup manager and bridge.py. Check processes are running, then engage and verify. @@ -44,7 +35,7 @@ class TestCarlaIntegration(unittest.TestCase): sm = messaging.SubMaster(['controlsState', 'carEvents', 'managerState']) q = Queue() - carla_bridge = CarlaBridge(bridge.parse_args(['--low_quality'])) + carla_bridge = CarlaBridge(bridge.parse_args([])) p_bridge = carla_bridge.run(q, retries=3) self.processes.append(p_bridge) @@ -70,7 +61,7 @@ class TestCarlaIntegration(unittest.TestCase): no_car_events_issues_once = True break - self.assertTrue(no_car_events_issues_once, f"Failed because sm offline, or CarEvents '{car_event_issues}' or processes not running '{not_running}'") + self.assertTrue(no_car_events_issues_once, f"Failed because no messages received, or CarEvents '{car_event_issues}' or processes not running '{not_running}'") start_time = time.monotonic() min_counts_control_active = 100 @@ -99,8 +90,11 @@ class TestCarlaIntegration(unittest.TestCase): p.wait(15) else: p.join(15) - subprocess.run("docker rm -f carla_sim", shell=True, stderr=subprocess.PIPE, check=False) + # Stop carla simulator by removing docker container + subprocess.run("docker rm -f carla_sim", shell=True, stderr=subprocess.PIPE, check=False) + if self.carla_process is not None: + self.carla_process.wait() if __name__ == "__main__":