robosim.robosim

Minimal raylib-based UAV Trajectory Simulation with TCP connection to control the drone. Constructs a simple obstacle world (cubes on a grid).

Keybinds:

  • Controls on cameras (world, topdown, FPV):
    • W, A, S, D (both cameras) + mouse for rotation (just for world camera).
    • Q, E, arrows, PgUp, PgDown (just for FPV)
  • Misc:
    • F1/F2/F3: Switch to world camera (3D overview) / top-dopwn camera (2D view) / FPV camera (UAV)
    • ESC: Quit
  1#!/usr/bin/env python3
  2"""
  3Minimal raylib-based UAV Trajectory Simulation with TCP connection to control the drone.
  4Constructs a simple obstacle world (cubes on a grid).
  5
  6Keybinds:
  7- Controls on cameras (world, topdown, FPV):
  8  - W, A, S, D (both cameras) + mouse for rotation (just for world camera).
  9  - Q, E, arrows, PgUp, PgDown (just for FPV)
 10- Misc:
 11  - F1/F2/F3: Switch to world camera (3D overview) / top-dopwn camera (2D view) / FPV camera (UAV)
 12  - ESC: Quit
 13"""
 14from __future__ import annotations
 15from pathlib import Path
 16from functools import partial
 17from argparse import ArgumentParser, Namespace
 18import json
 19import sys
 20import time
 21
 22import numpy as np
 23import raylib as rl
 24from microspec import Protocol
 25from micronetcode import Message, ConnectionManager
 26
 27from robolib.utils import logger, get_project_root, FixedSizeDict, rl_get_device_and_renderer
 28from robolib.camera import Camera
 29from robolib.constants import SCREEN_HEIGHT, SCREEN_WIDTH, DT
 30from robolib.components import HasModel, HasPose, HasFPV, HasMotion
 31from robolib.systems import FPVRenderSystem, PhysicsSystem, RenderSystem
 32from robolib.netcode import MsgpackCodec, RobosimClient
 33from robolib.serialization import world_from_dict
 34
 35from plugins import REGISTERED_PLUGINS
 36
 37np.set_printoptions(precision=3, linewidth=120)
 38rl.SetTraceLogLevel(rl.LOG_WARNING)
 39
 40sys.path.append(str(get_project_root() / "src/robosim/"))
 41from simulator_singleton import Simulator, SimState # pylint: disable=wrong-import-order, import-error, wrong-import-position
 42from protocol import io_handler, network_handler, responses_handler, SCRIPT_CLIENT # pylint: disable=wrong-import-order, import-error, wrong-import-position
 43
 44# global settings and types
 45
 46PROTOCOL_PATH = get_project_root() / "src/robosim/protocol.json"
 47INIT_CONFIG: dict | None = None
 48
 49def _build_script_lines(script_path: Path) -> dict[int, list[Message]]:
 50    script_lines = {}
 51    with open(script_path, "r") as fp:
 52        for script_line in map(lambda x: x.strip(), fp.readlines()):
 53            if len(script_line) == 0 or script_line.startswith("#"): # comment or empty line
 54                continue
 55
 56            raw_timestamp, cmd, raw_args = script_line.split(" ", maxsplit=2)
 57            parsed_args = json.loads(raw_args)
 58            if "." in raw_timestamp or "-" in raw_timestamp:
 59                raise ValueError(f"Negative or floating point timestamp: {raw_timestamp}")
 60
 61            data = {"cmd": cmd, **parsed_args}
 62            script_lines.setdefault(int(raw_timestamp), []).append(Message("script", data, client=SCRIPT_CLIENT))
 63
 64    return script_lines
 65
 66def rl_init(headless: bool):
 67    """Initialize the window with proper flags, full screen (windowed) mode based on monitor width/height etc."""
 68    rl.SetConfigFlags(rl.FLAG_WINDOW_HIDDEN | rl.FLAG_WINDOW_UNDECORATED if headless else rl.FLAG_WINDOW_RESIZABLE)
 69    rl.InitWindow(SCREEN_WIDTH, SCREEN_HEIGHT, b"UAV Trajectory Simulation")
 70    rl.SetWindowPosition(20, 200)
 71    rl.SetTargetFPS(0)
 72    rl.DisableEventWaiting()
 73    rl.rlEnableBackfaceCulling()
 74    rl.SetExitKey(rl.KEY_NULL)
 75
 76def main(args: Namespace) -> list[dict]:
 77    """main fn"""
 78    global INIT_CONFIG # pylint: disable=global-statement
 79    rl_init(args.headless)
 80
 81    with open(args.map_path, "r") as fp:
 82        world = world_from_dict(json.load(fp)["world"])
 83    world.update()
 84
 85    state = SimState()
 86    conn_manager = ConnectionManager("0.0.0.0", args.port, codec=MsgpackCodec(), max_connections=args.max_connections,
 87                                     client_type=RobosimClient)
 88
 89    with open(PROTOCOL_PATH, "r") as fp:
 90        protocol = Protocol.from_dict(json.load(fp), max_connection_id=args.max_connections - 1,
 91                                      components=world.component_names)
 92
 93    script_lines, script_responses = {}, []
 94    if args.script is not None:
 95        script_lines = _build_script_lines(args.script)
 96        logger.info(f"Read {sum(len(x) for x in script_lines.values())} script lines from: '{args.script}'")
 97
 98    sim = Simulator(world=world, state=state, connection_manager=conn_manager, plugins=args.plugins, protocol=protocol)
 99    logger.info("Created default map" if args.map_path is None else f"Constructed map from '{args.map_path}'")
100    logger.info(f"Created {len(world.query(HasModel, exclude=[HasFPV]))} objects and {len(world.query(HasFPV))} robots")
101    logger.info(f"Created simulator object:\n{sim}")
102    logger.info(f"Raylib rendering on: {' / '.join(rl_get_device_and_renderer())}")
103    INIT_CONFIG = sim.to_dict()
104    if len(world.query(HasFPV)) == 0:
105        logger.warning("No robots found in the world. Add HasFPV component to existing entities")
106
107    conn_manager.on_disconnect = sim.client_on_disconnect
108    conn_manager.network_handler = partial(network_handler, sim=sim)
109    conn_manager.start()
110
111    fpv_render_system = FPVRenderSystem()
112    physics_system = PhysicsSystem()
113    render_system = RenderSystem()
114
115    cam: Camera
116    while True:
117        sim.world.update()
118        sim.clock.wait_and_tick()
119
120        # I/O systems: read messages from the keyboard as well as all robots (via plugins/channels)
121        if sim.state.must_exit or not conn_manager.is_alive():
122            break
123        sim.world.query(HasMotion).motion_input = 0 # reset all the inputs for the physics system
124        script_msgs_this_tick = script_lines.get(sim.clock.tick_count, [])
125        message_responses = io_handler(sim, extra_messages=script_msgs_this_tick, init_config=INIT_CONFIG)
126
127        # Update systems
128        if not args.headless:
129            sim.active_camera.manual_movement()
130        # Update FPV cameras based on current positions (TODO: should be somewhere else)
131        qr = sim.world.query(HasPose, HasFPV)
132        for cam, pose in zip(map(lambda x: x.item(), qr.fpv_camera), qr.pose):
133            cam.set_pose(position=pose[0:3, 3], up=pose[0:3, 1], target=(pose[0:3, 3] + pose[0:3, 2]))
134
135        if sim.state.display_uav_trace:
136            robots = sim.world.query(HasFPV, HasPose)
137            for robot_id, pose in zip(robots.entity_ids, robots.pose):
138                robot_traces = sim.state.uav_traces.setdefault(
139                    int(robot_id), FixedSizeDict(maxlen=sim.state.uav_traces_max_len))
140                robot_traces[tuple(pose[0:3, 3].tolist())] = True
141
142        # Physics systems + the plugins callbacks before/after it
143        prev = time.perf_counter()
144        sim.plugins_manager.on_before_physics(sim.world)
145        for _ in sim.clock.drain():
146            physics_system(world=sim.world, dt=DT, cell_size=sim.collision_cell_size)
147        sim.plugins_manager.on_after_physics(sim.world)
148        sim.physics_ticks_stats.append(time.perf_counter() - prev)
149
150        # Rendering: first fpv cameras (for each robot), then the main UI camera
151        rl.BeginDrawing()
152        prev = time.perf_counter()
153        camera_entity_id = sim.robot_eids[sim.state.fpv_ix] if sim.active_camera.label == "FPV" else None
154        if not args.headless:
155            render_system(sim.world, sim.active_camera, extra_drawables=sim.plugins_manager.plugins,
156                          camera_entity_id=camera_entity_id, collision_mode=sim.state.collision_render_mode,
157                          wireframe_mode=sim.state.wireframe_mode, display_uav_trace=sim.state.display_uav_trace,
158                          uav_traces=sim.state.uav_traces, display_hud=True)
159        fpv_render_system(world=sim.world, connected_eids=list(sim.robot_eid_to_channel_ix),
160                          extra_drawables=sim.plugins_manager.plugins)
161        sim.render_ticks_stats.append(time.perf_counter() - prev)
162        rl.EndDrawing()
163
164        responses_handler(sim, message_responses, script_responses)
165
166    rl.CloseWindow()
167    conn_manager.close()
168    return script_responses
169
170if __name__ == "__main__":
171    parser = ArgumentParser()
172    parser.add_argument("--map_path", type=Path, help="Path to the map config (scene objects, not robots). "
173                                                      "If not set, the default scene is loaded (maps/01-basic.json)")
174    parser.add_argument("--headless", action="store_true", help="If set, raylib is started in headless mode")
175    parser.add_argument("--port", "-p", type=int, help="The port for the TCP listener", default=42069)
176    parser.add_argument("--max_connections", type=int, default=2, help="The maximum number of robot connections")
177    parser.add_argument("--plugins", nargs="*", default=["manual_move"],
178                        help=f"Which plugins to start, if any. Registered: {list(REGISTERED_PLUGINS)}")
179    parser.add_argument("--script", "-i", type=Path, help="Optional path to a scrip to be ran at start time")
180    arg = parser.parse_args()
181    assert len(set(arg.plugins)) == len(arg.plugins), f"duplicates in --plugins: {arg.plugins}"
182    if arg.map_path is None:
183        arg.map_path = get_project_root() / "resources/maps/01-basic.json"
184        logger.info(f"No --map_path provided. Defaulting to: '{arg.map_path}'")
185    main(arg)
PROTOCOL_PATH = PosixPath('/builds/open-visual-robotics/robosim/src/robosim/protocol.json')
INIT_CONFIG: dict | None = None
def rl_init(headless: bool):
67def rl_init(headless: bool):
68    """Initialize the window with proper flags, full screen (windowed) mode based on monitor width/height etc."""
69    rl.SetConfigFlags(rl.FLAG_WINDOW_HIDDEN | rl.FLAG_WINDOW_UNDECORATED if headless else rl.FLAG_WINDOW_RESIZABLE)
70    rl.InitWindow(SCREEN_WIDTH, SCREEN_HEIGHT, b"UAV Trajectory Simulation")
71    rl.SetWindowPosition(20, 200)
72    rl.SetTargetFPS(0)
73    rl.DisableEventWaiting()
74    rl.rlEnableBackfaceCulling()
75    rl.SetExitKey(rl.KEY_NULL)

Initialize the window with proper flags, full screen (windowed) mode based on monitor width/height etc.

def main(args: argparse.Namespace) -> list[dict]:
 77def main(args: Namespace) -> list[dict]:
 78    """main fn"""
 79    global INIT_CONFIG # pylint: disable=global-statement
 80    rl_init(args.headless)
 81
 82    with open(args.map_path, "r") as fp:
 83        world = world_from_dict(json.load(fp)["world"])
 84    world.update()
 85
 86    state = SimState()
 87    conn_manager = ConnectionManager("0.0.0.0", args.port, codec=MsgpackCodec(), max_connections=args.max_connections,
 88                                     client_type=RobosimClient)
 89
 90    with open(PROTOCOL_PATH, "r") as fp:
 91        protocol = Protocol.from_dict(json.load(fp), max_connection_id=args.max_connections - 1,
 92                                      components=world.component_names)
 93
 94    script_lines, script_responses = {}, []
 95    if args.script is not None:
 96        script_lines = _build_script_lines(args.script)
 97        logger.info(f"Read {sum(len(x) for x in script_lines.values())} script lines from: '{args.script}'")
 98
 99    sim = Simulator(world=world, state=state, connection_manager=conn_manager, plugins=args.plugins, protocol=protocol)
100    logger.info("Created default map" if args.map_path is None else f"Constructed map from '{args.map_path}'")
101    logger.info(f"Created {len(world.query(HasModel, exclude=[HasFPV]))} objects and {len(world.query(HasFPV))} robots")
102    logger.info(f"Created simulator object:\n{sim}")
103    logger.info(f"Raylib rendering on: {' / '.join(rl_get_device_and_renderer())}")
104    INIT_CONFIG = sim.to_dict()
105    if len(world.query(HasFPV)) == 0:
106        logger.warning("No robots found in the world. Add HasFPV component to existing entities")
107
108    conn_manager.on_disconnect = sim.client_on_disconnect
109    conn_manager.network_handler = partial(network_handler, sim=sim)
110    conn_manager.start()
111
112    fpv_render_system = FPVRenderSystem()
113    physics_system = PhysicsSystem()
114    render_system = RenderSystem()
115
116    cam: Camera
117    while True:
118        sim.world.update()
119        sim.clock.wait_and_tick()
120
121        # I/O systems: read messages from the keyboard as well as all robots (via plugins/channels)
122        if sim.state.must_exit or not conn_manager.is_alive():
123            break
124        sim.world.query(HasMotion).motion_input = 0 # reset all the inputs for the physics system
125        script_msgs_this_tick = script_lines.get(sim.clock.tick_count, [])
126        message_responses = io_handler(sim, extra_messages=script_msgs_this_tick, init_config=INIT_CONFIG)
127
128        # Update systems
129        if not args.headless:
130            sim.active_camera.manual_movement()
131        # Update FPV cameras based on current positions (TODO: should be somewhere else)
132        qr = sim.world.query(HasPose, HasFPV)
133        for cam, pose in zip(map(lambda x: x.item(), qr.fpv_camera), qr.pose):
134            cam.set_pose(position=pose[0:3, 3], up=pose[0:3, 1], target=(pose[0:3, 3] + pose[0:3, 2]))
135
136        if sim.state.display_uav_trace:
137            robots = sim.world.query(HasFPV, HasPose)
138            for robot_id, pose in zip(robots.entity_ids, robots.pose):
139                robot_traces = sim.state.uav_traces.setdefault(
140                    int(robot_id), FixedSizeDict(maxlen=sim.state.uav_traces_max_len))
141                robot_traces[tuple(pose[0:3, 3].tolist())] = True
142
143        # Physics systems + the plugins callbacks before/after it
144        prev = time.perf_counter()
145        sim.plugins_manager.on_before_physics(sim.world)
146        for _ in sim.clock.drain():
147            physics_system(world=sim.world, dt=DT, cell_size=sim.collision_cell_size)
148        sim.plugins_manager.on_after_physics(sim.world)
149        sim.physics_ticks_stats.append(time.perf_counter() - prev)
150
151        # Rendering: first fpv cameras (for each robot), then the main UI camera
152        rl.BeginDrawing()
153        prev = time.perf_counter()
154        camera_entity_id = sim.robot_eids[sim.state.fpv_ix] if sim.active_camera.label == "FPV" else None
155        if not args.headless:
156            render_system(sim.world, sim.active_camera, extra_drawables=sim.plugins_manager.plugins,
157                          camera_entity_id=camera_entity_id, collision_mode=sim.state.collision_render_mode,
158                          wireframe_mode=sim.state.wireframe_mode, display_uav_trace=sim.state.display_uav_trace,
159                          uav_traces=sim.state.uav_traces, display_hud=True)
160        fpv_render_system(world=sim.world, connected_eids=list(sim.robot_eid_to_channel_ix),
161                          extra_drawables=sim.plugins_manager.plugins)
162        sim.render_ticks_stats.append(time.perf_counter() - prev)
163        rl.EndDrawing()
164
165        responses_handler(sim, message_responses, script_responses)
166
167    rl.CloseWindow()
168    conn_manager.close()
169    return script_responses

main fn