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 microecs import World, EntityId
 25from microspec import Protocol
 26from micronetcode import Message, ConnectionManager
 27
 28from robolib.utils import (Clock, logger, get_project_root, get_closest_square,
 29                           pose_from_position_target_up, pose_from_trans_euler as pte, make_arr, FixedSizeDict,
 30                           rl_get_device_and_renderer)
 31from robolib.constants import (SCREEN_HEIGHT, SCREEN_WIDTH, DT, MAX_SUBTICKS_PER_RENDER_TICK,
 32                               LVL2_DRAG_COEFF, LVL2_MAX_ACCELERATIONS)
 33from robolib.physics import make_collision_cell_size
 34from robolib.entities import add_entity
 35from robolib.components import (ColliderKinds, HasModel, HasPose, HasFPV,
 36                                HasMotionInput, HasCollision, HasTag, HasPhysicsLevel1, HasPhysicsLevel2,
 37                                HasVelocity, HasAcceleration)
 38from robolib.systems import FPVCameraSystem, PhysicsSystem
 39from robolib.netcode import MsgpackCodec, RobosimClient
 40
 41from plugins import REGISTERED_PLUGINS
 42
 43np.set_printoptions(precision=3, linewidth=120)
 44rl.SetTraceLogLevel(rl.LOG_WARNING)
 45
 46sys.path.append(str(get_project_root() / "src/robosim/"))
 47from simulator_singleton import ALL_COMPONENTS, Simulator, SimState, world_from_dict, GroundFloor # pylint: disable=wrong-import-order, import-error, wrong-import-position
 48from protocol import network_slow_handler, network_handler, _handle_message # pylint: disable=wrong-import-order, import-error, wrong-import-position
 49
 50# global settings and types
 51
 52RESOURCES_PATH = get_project_root() / "resources"
 53INIT_CONFIG: dict | None = None
 54
 55# default map stuff
 56
 57def _make_cube(world: World, pose: np.ndarray, scale: float) -> EntityId:
 58    base_components = [HasTag, HasModel, HasPose]
 59    cube_obj = RESOURCES_PATH / "models/cube/cube.obj"
 60    crate_png = RESOURCES_PATH / "textures/crate.png"
 61    aabb = ColliderKinds.AABB
 62    cb_args = {"tag": "cube", "model_path": str(cube_obj), "texture_path": str(crate_png),
 63                "scale": [scale], "pose": pose, "collider_kind": [aabb]}
 64    return add_entity(world, base_components + [HasCollision], **cb_args)
 65
 66def build_default_scene_objects() -> World:
 67    """builds the default basic scene objects for the simulator: n robots + a few obstacles"""
 68    # NOTE: if plugins add their own components, ideally they should be in a separate plugin-owned world! Not enforced..
 69    world = World(components=ALL_COMPONENTS, extra_metadata=["serializable", "comment"])
 70
 71    grass_png = RESOURCES_PATH / "textures/grass.png"
 72    ground_obj = RESOURCES_PATH / "models/ground/ground.obj"
 73    house_obj = RESOURCES_PATH / "models/house/house.obj"
 74    aabb = ColliderKinds.AABB
 75    base_components = [HasTag, HasModel, HasPose]
 76    make_pte = lambda *data: pte(make_arr(*data))
 77
 78    # Ground plane
 79    gp_args = {"tag": "ground", "model_path": str(ground_obj), "texture_path": str(grass_png),
 80               "scale": [20.0], "pose": make_pte(0, 0, 0, 0, 0, 0), "collider_kind": [aabb]}
 81    eid = add_entity(world, components=base_components + [GroundFloor, HasCollision], **gp_args)
 82    world.update()
 83    ground_entity = world.get_entity(eid)
 84    height = (ground_entity.model[0].bbox[1][1] * ground_entity.scale).item()
 85    # Some scattered cubes at ground level (height (0.25) + 0.5 (box height//2) == ground level)
 86    _make_cube(world, make_pte(5.0,  height + 0.5,  5.0, 0, 0, 0), scale=1)
 87    _make_cube(world, make_pte(5.0,  height + 0.5, -3.0, 0, 0, 0), scale=1)
 88    _make_cube(world, make_pte(-4.0, height + 0.5,  2.0, 0, 0, 0), scale=1)
 89    _make_cube(world, make_pte(-6.0, height + 0.5, -5.0, 0, 0, 0), scale=1)
 90    _make_cube(world, make_pte(8.0,  height + 0.5,  0.0, 0, 0, 0), scale=1)
 91    # A small tower (stacked cubes)
 92    _make_cube(world, make_pte(-2.0,     height + 0.5, -2.0, 0, 0, 0), scale=1)
 93    _make_cube(world, make_pte(-2.0, 2 * height + 0.5, -2.0, 0, 0, 0), scale=1)
 94    _make_cube(world, make_pte(-2.0, 3 * height + 0.5, -2.0, 0, 0, 0), scale=1)
 95    # Floating obstacles (for UAV to navigate around)
 96    _make_cube(world, make_pte(3.0,  3.0,  3.0, 0, 0, 0), scale=1)
 97    _make_cube(world, make_pte(-3.0, 4.0,  0.0, 0, 0, 0), scale=1)
 98    _make_cube(world, make_pte(0.0,  5.0, -4.0, 0, 0, 0), scale=1)
 99    # Directly in UAV's POV at start (UAV at y=6, looking +Z)
100    _make_cube(world, make_pte(0.0, 5.5, 8.0, 0, 0, 0), scale=1)
101    # House model
102    house_args = {"tag": "house", "model_path": str(house_obj), "texture_path": None,
103                  "scale": [0.1], "pose": make_pte(5, 2, 0, 0, 0, 0)}
104    add_entity(world, components=base_components, **house_args)
105    return world
106
107def build_robots(world: World, n_robots: int, robot_models: list[str]):
108    """build the robots in the scene"""
109    _, c = get_closest_square(n_robots)
110    model_paths = [RESOURCES_PATH/ "models" / x / f"{x}.obj" for x in robot_models]
111    scales = [0.8] * len(model_paths)
112
113    # physics_type = "level1"
114    # kwargs = {
115    #     "max_velocities": np.float32(LVL1_MAX_VELOCITIES),
116    # }
117    physics_type = "level2"
118    physics_args = {
119        "max_accelerations": np.float32(LVL2_MAX_ACCELERATIONS),
120        "drag_coefficient": np.float32((LVL2_DRAG_COEFF, )),
121    }
122
123    for i in range(n_robots):
124        pose = pose_from_position_target_up(position=make_arr(i % c, 6, i // c),
125                                            target=make_arr(i % c, 6, i // c + 1), up=make_arr(0, 1, 0))
126        model_path = model_paths[i % len(model_paths)]
127        scale = scales[i % len(model_paths)]
128        components = [
129            HasTag, HasModel, HasPose, HasFPV,
130            HasPhysicsLevel1 if physics_type == "level1" else HasPhysicsLevel2,
131            HasVelocity, HasAcceleration, HasMotionInput, HasCollision,
132        ]
133
134        data = {
135            "tag": "robot",
136            "model_path": str(model_path),
137            "texture_path": None,
138            "scale": [scale],
139            "pose": pose,
140            "collider_kind": [ColliderKinds.SPHERE],
141            **physics_args,
142        }
143        add_entity(world, components, **data)
144
145def keyboard_handler(sim: Simulator):
146    """
147    Handle keyboard input for the main UI. We create a message in the same protocol of robosim-ncat/global hanlder
148    Note: network handler is in protocol.py definit the entire protocol handlers of protocol.json.
149    """
150    msgs: list[dict] = []
151
152    # camera management
153    if rl.IsKeyPressed(rl.KEY_F1):
154        msgs += [{"cmd": "sim_set_camera", "type": "world", "id": 0}] # dummy ids, needed for protocol
155    if rl.IsKeyPressed(rl.KEY_F2):
156        msgs += [{"cmd": "sim_set_camera", "type": "topdown", "id": 0}] # dummy ids, needed for protocol
157    if rl.IsKeyPressed(rl.KEY_F3): # we still keep the 'fpv' camera here but cannot control it.
158        if sim.state.active_camera_type == "fpv":
159            sim.state.fpv_ix = (sim.state.fpv_ix + 1) % len(sim.world.query(HasFPV))
160        msgs += [{"cmd": "sim_set_camera", "type": "fpv", "id": sim.state.fpv_ix}]
161
162    # sim state management
163    if rl.IsKeyPressed(rl.KEY_F5): # save the state of the scene on the server side
164        msgs += [{"cmd": "sim_save_state"}]
165    if rl.IsKeyPressed(rl.KEY_F6): # load the state of the scene on the server side
166        try:
167            with open(RESOURCES_PATH/"state.json", "r") as fp:
168                state = json.load(fp)
169            msgs += [{"cmd": "sim_load_state", "state": state}]
170        except Exception as e:
171            logger.error(str(e))
172    if rl.IsKeyPressed(rl.KEY_R):
173        msgs += [{"cmd": "sim_reset"}]
174
175    # render management
176    if rl.IsKeyPressed(rl.KEY_I):
177        msgs += [{"cmd": "sim_set_wireframe_mode", "value": not sim.state.wireframe_mode}]
178    if rl.IsKeyPressed(rl.KEY_O):
179        msgs += [{"cmd": "sim_set_collision_render_mode", "value": not sim.state.collision_render_mode}]
180
181    # move robot via keyboard
182    if sim.state.active_camera_type == "fpv":
183        entity_id = sim.robot_eids[sim.state.fpv_ix]
184        control_input = np.float32([
185            rl.IsKeyDown(rl.KEY_A)       - rl.IsKeyDown(rl.KEY_D),           # left/right (x)
186            rl.IsKeyDown(rl.KEY_PAGE_UP) - rl.IsKeyDown(rl.KEY_PAGE_DOWN),   # up/down (y)
187            rl.IsKeyDown(rl.KEY_W)       - rl.IsKeyDown(rl.KEY_S),           # forward/backward (z)
188            rl.IsKeyDown(rl.KEY_DOWN)    - rl.IsKeyDown(rl.KEY_UP),          # pitch
189            rl.IsKeyDown(rl.KEY_Q)       - rl.IsKeyDown(rl.KEY_E),           # yaw
190            rl.IsKeyDown(rl.KEY_RIGHT)   - rl.IsKeyDown(rl.KEY_LEFT),        # roll
191        ])
192        if (control_input != 0).any():
193            msgs += [{"cmd": "entity_set_data", "entity_id": entity_id, "component": "HasMotionInput",
194                     "data": {"motion_input": control_input}}]
195
196    # misc / other commands
197    if rl.IsKeyPressed(rl.KEY_F9):
198        msgs += [{"cmd": "sim_set_collision_cell_size", "value": np.maximum(sim.state.collision_cell_size - 0.1, 0.1)}]
199    if rl.IsKeyPressed(rl.KEY_F10):
200        msgs += [{"cmd": "sim_set_collision_cell_size", "value": np.minimum(sim.state.collision_cell_size + 0.1, 30)}]
201    if rl.IsKeyPressed(rl.KEY_T):
202        msgs += [{"cmd": "sim_display_uav_trace", "value": not sim.state.display_uav_trace}]
203
204    for msg in msgs:
205        res = _handle_message(sim, Message("script", data=msg), init_config=INIT_CONFIG)
206        if "error" in res:
207            logger.error(res["error"])
208        else:
209            if not (msg["cmd"] == "entity_set_data" and msg.get("component", "") == "HasMotionInput"):
210                logger.debug(res["status"])
211
212# main and cli args
213
214def main(args: Namespace):
215    """main fn"""
216    global INIT_CONFIG # pylint: disable=global-statement
217    rl.SetConfigFlags(rl.FLAG_WINDOW_HIDDEN | rl.FLAG_WINDOW_UNDECORATED if args.headless else rl.FLAG_WINDOW_RESIZABLE)
218    rl.InitWindow(SCREEN_WIDTH, SCREEN_HEIGHT, b"UAV Trajectory Simulation")
219    rl.SetWindowPosition(20, 200)
220    rl.SetTargetFPS(0)
221    rl.DisableEventWaiting()
222    rl.rlEnableBackfaceCulling()
223
224    if args.map_path is None:
225        world = build_default_scene_objects()
226    else:
227        with open(args.map_path, "r") as fp:
228            world = world_from_dict(json.load(fp)["world"])
229
230    world.update()
231    if len(world.query(HasFPV)) == 0:
232        build_robots(world, args.n_robots, args.robot_models)
233
234    state = SimState(collision_cell_size=make_collision_cell_size(world))
235    conn_manager = ConnectionManager("0.0.0.0", args.port, codec=MsgpackCodec(), max_connections=args.n_robots,
236                                     client_type=RobosimClient)
237
238    with open(args.protocol_path, "r") as fp:
239        protocol = Protocol.from_dict(json.load(fp), max_robot_ix=args.n_robots - 1, components=world.component_names)
240    sim = Simulator(world=world, state=state, connection_manager=conn_manager, plugins=args.plugins, protocol=protocol)
241    logger.info("Created default map" if args.map_path is None else f"Constructed map from '{args.map_path}'")
242    logger.info(f"Created {len(world.query(HasModel, exclude=[HasFPV]))} objects and {len(world.query(HasFPV))} robots")
243    logger.info(f"Created simulator object:\n{sim}")
244    logger.info(f"Raylib rendering on: {' / '.join(rl_get_device_and_renderer())}")
245    INIT_CONFIG = sim.to_dict()
246
247    conn_manager.on_disconnect = sim.client_on_disconnect
248    conn_manager.network_handler = partial(network_handler, sim=sim)
249    conn_manager.start()
250
251    fpv_system = FPVCameraSystem()
252    physics_system = PhysicsSystem()
253
254    clock = Clock(dt=DT, max_ticks=MAX_SUBTICKS_PER_RENDER_TICK)
255    while True:
256        sim.world.update()
257        clock.wait_and_tick()
258
259        # I/O systems: read messages from the keyboard as well as all robots (via plugins/channels)
260        if rl.IsKeyPressed(rl.KEY_ESCAPE) or not conn_manager.is_alive():
261            break
262        sim.world.query(HasMotionInput).motion_input = 0 # reset all the inputs for the physics system
263        keyboard_handler(sim)
264        network_slow_handler(sim, init_config=INIT_CONFIG)
265
266        # Update systems
267        if not args.headless:
268            sim.active_camera.manual_movement()
269        fpv_system(world=sim.world)
270
271        if sim.state.display_uav_trace:
272            robots = sim.world.query(HasFPV, HasPose)
273            for robot_id, pose in zip(robots.entity_ids, robots.pose):
274                robot_traces = sim.state.uav_traces.setdefault(
275                    int(robot_id), FixedSizeDict(maxlen=sim.state.uav_traces_max_len))
276                robot_traces[tuple(pose[0:3, 3].tolist())] = True
277
278        # Physics systems + the plugins callbacks before/after it
279        prev = time.perf_counter()
280        sim.plugins_manager.on_before_physics(sim.world)
281        for _ in clock.drain():
282            physics_system(world=sim.world, dt=DT, cell_size=sim.state.collision_cell_size)
283        sim.plugins_manager.on_after_physics(sim.world)
284        sim.physics_ticks_stats.append(time.perf_counter() - prev)
285
286        # Rendering: first fpv cameras (for each robot), then the main UI camera
287        prev = time.perf_counter()
288        sim.render_fpv_cameras(extra_drawables=sim.plugins_manager.plugins)
289        if not args.headless:
290            sim.render_main_camera(extra_drawables=sim.plugins_manager.plugins)
291        sim.render_ticks_stats.append(time.perf_counter() - prev)
292
293        sim.plugins_manager.responses_handler(world=sim.world)
294
295    rl.CloseWindow()
296
297if __name__ == "__main__":
298    parser = ArgumentParser()
299    parser.add_argument("--map_path", type=Path, help="Path to the map config (scene objects, not robots). "
300                                                      "If not set, manual scene is built")
301    parser.add_argument("--headless", action="store_true", help="If set, raylib is started in headless mode")
302    parser.add_argument("--port", "-p", type=int, help="The port for the TCP listener", default=42069)
303    parser.add_argument("--n_robots", type=int, default=2, help="The maximum number of robots (connectable) allowed")
304    parser.add_argument("--robot_models", nargs="+", help="A list of models for robots. If not set, uses all available")
305    parser.add_argument("--plugins", nargs="*", default=["manual_move"],
306                        help=f"Which plugins to start, if any. Registered: {list(REGISTERED_PLUGINS)}")
307    parser.add_argument("--protocol_path", type=Path, default=get_project_root() / "src/robosim/protocol.json")
308    arg = parser.parse_args()
309    assert len(set(arg.plugins)) == len(arg.plugins), f"duplicates in --plugins: {arg.plugins}"
310    if arg.robot_models is None:
311        arg.robot_models = [x.name for x in (RESOURCES_PATH/"models").iterdir()
312                             if x.name.startswith("drone") and x.name != "drone_cube_head"]
313        logger.info(f"--robot_models not provided, using all found: {arg.robot_models}")
314    assert arg.protocol_path.exists(), f"Protocol file (protocol.json) not found at: '{arg.protocol_path}'"
315    main(arg)
RESOURCES_PATH = PosixPath('/builds/open-visual-robotics/robosim/resources')
INIT_CONFIG: dict | None = None
def build_default_scene_objects() -> microecs.world.World:
 67def build_default_scene_objects() -> World:
 68    """builds the default basic scene objects for the simulator: n robots + a few obstacles"""
 69    # NOTE: if plugins add their own components, ideally they should be in a separate plugin-owned world! Not enforced..
 70    world = World(components=ALL_COMPONENTS, extra_metadata=["serializable", "comment"])
 71
 72    grass_png = RESOURCES_PATH / "textures/grass.png"
 73    ground_obj = RESOURCES_PATH / "models/ground/ground.obj"
 74    house_obj = RESOURCES_PATH / "models/house/house.obj"
 75    aabb = ColliderKinds.AABB
 76    base_components = [HasTag, HasModel, HasPose]
 77    make_pte = lambda *data: pte(make_arr(*data))
 78
 79    # Ground plane
 80    gp_args = {"tag": "ground", "model_path": str(ground_obj), "texture_path": str(grass_png),
 81               "scale": [20.0], "pose": make_pte(0, 0, 0, 0, 0, 0), "collider_kind": [aabb]}
 82    eid = add_entity(world, components=base_components + [GroundFloor, HasCollision], **gp_args)
 83    world.update()
 84    ground_entity = world.get_entity(eid)
 85    height = (ground_entity.model[0].bbox[1][1] * ground_entity.scale).item()
 86    # Some scattered cubes at ground level (height (0.25) + 0.5 (box height//2) == ground level)
 87    _make_cube(world, make_pte(5.0,  height + 0.5,  5.0, 0, 0, 0), scale=1)
 88    _make_cube(world, make_pte(5.0,  height + 0.5, -3.0, 0, 0, 0), scale=1)
 89    _make_cube(world, make_pte(-4.0, height + 0.5,  2.0, 0, 0, 0), scale=1)
 90    _make_cube(world, make_pte(-6.0, height + 0.5, -5.0, 0, 0, 0), scale=1)
 91    _make_cube(world, make_pte(8.0,  height + 0.5,  0.0, 0, 0, 0), scale=1)
 92    # A small tower (stacked cubes)
 93    _make_cube(world, make_pte(-2.0,     height + 0.5, -2.0, 0, 0, 0), scale=1)
 94    _make_cube(world, make_pte(-2.0, 2 * height + 0.5, -2.0, 0, 0, 0), scale=1)
 95    _make_cube(world, make_pte(-2.0, 3 * height + 0.5, -2.0, 0, 0, 0), scale=1)
 96    # Floating obstacles (for UAV to navigate around)
 97    _make_cube(world, make_pte(3.0,  3.0,  3.0, 0, 0, 0), scale=1)
 98    _make_cube(world, make_pte(-3.0, 4.0,  0.0, 0, 0, 0), scale=1)
 99    _make_cube(world, make_pte(0.0,  5.0, -4.0, 0, 0, 0), scale=1)
100    # Directly in UAV's POV at start (UAV at y=6, looking +Z)
101    _make_cube(world, make_pte(0.0, 5.5, 8.0, 0, 0, 0), scale=1)
102    # House model
103    house_args = {"tag": "house", "model_path": str(house_obj), "texture_path": None,
104                  "scale": [0.1], "pose": make_pte(5, 2, 0, 0, 0, 0)}
105    add_entity(world, components=base_components, **house_args)
106    return world

builds the default basic scene objects for the simulator: n robots + a few obstacles

def build_robots(world: microecs.world.World, n_robots: int, robot_models: list[str]):
108def build_robots(world: World, n_robots: int, robot_models: list[str]):
109    """build the robots in the scene"""
110    _, c = get_closest_square(n_robots)
111    model_paths = [RESOURCES_PATH/ "models" / x / f"{x}.obj" for x in robot_models]
112    scales = [0.8] * len(model_paths)
113
114    # physics_type = "level1"
115    # kwargs = {
116    #     "max_velocities": np.float32(LVL1_MAX_VELOCITIES),
117    # }
118    physics_type = "level2"
119    physics_args = {
120        "max_accelerations": np.float32(LVL2_MAX_ACCELERATIONS),
121        "drag_coefficient": np.float32((LVL2_DRAG_COEFF, )),
122    }
123
124    for i in range(n_robots):
125        pose = pose_from_position_target_up(position=make_arr(i % c, 6, i // c),
126                                            target=make_arr(i % c, 6, i // c + 1), up=make_arr(0, 1, 0))
127        model_path = model_paths[i % len(model_paths)]
128        scale = scales[i % len(model_paths)]
129        components = [
130            HasTag, HasModel, HasPose, HasFPV,
131            HasPhysicsLevel1 if physics_type == "level1" else HasPhysicsLevel2,
132            HasVelocity, HasAcceleration, HasMotionInput, HasCollision,
133        ]
134
135        data = {
136            "tag": "robot",
137            "model_path": str(model_path),
138            "texture_path": None,
139            "scale": [scale],
140            "pose": pose,
141            "collider_kind": [ColliderKinds.SPHERE],
142            **physics_args,
143        }
144        add_entity(world, components, **data)

build the robots in the scene

def keyboard_handler(sim: simulator_singleton.Simulator):
146def keyboard_handler(sim: Simulator):
147    """
148    Handle keyboard input for the main UI. We create a message in the same protocol of robosim-ncat/global hanlder
149    Note: network handler is in protocol.py definit the entire protocol handlers of protocol.json.
150    """
151    msgs: list[dict] = []
152
153    # camera management
154    if rl.IsKeyPressed(rl.KEY_F1):
155        msgs += [{"cmd": "sim_set_camera", "type": "world", "id": 0}] # dummy ids, needed for protocol
156    if rl.IsKeyPressed(rl.KEY_F2):
157        msgs += [{"cmd": "sim_set_camera", "type": "topdown", "id": 0}] # dummy ids, needed for protocol
158    if rl.IsKeyPressed(rl.KEY_F3): # we still keep the 'fpv' camera here but cannot control it.
159        if sim.state.active_camera_type == "fpv":
160            sim.state.fpv_ix = (sim.state.fpv_ix + 1) % len(sim.world.query(HasFPV))
161        msgs += [{"cmd": "sim_set_camera", "type": "fpv", "id": sim.state.fpv_ix}]
162
163    # sim state management
164    if rl.IsKeyPressed(rl.KEY_F5): # save the state of the scene on the server side
165        msgs += [{"cmd": "sim_save_state"}]
166    if rl.IsKeyPressed(rl.KEY_F6): # load the state of the scene on the server side
167        try:
168            with open(RESOURCES_PATH/"state.json", "r") as fp:
169                state = json.load(fp)
170            msgs += [{"cmd": "sim_load_state", "state": state}]
171        except Exception as e:
172            logger.error(str(e))
173    if rl.IsKeyPressed(rl.KEY_R):
174        msgs += [{"cmd": "sim_reset"}]
175
176    # render management
177    if rl.IsKeyPressed(rl.KEY_I):
178        msgs += [{"cmd": "sim_set_wireframe_mode", "value": not sim.state.wireframe_mode}]
179    if rl.IsKeyPressed(rl.KEY_O):
180        msgs += [{"cmd": "sim_set_collision_render_mode", "value": not sim.state.collision_render_mode}]
181
182    # move robot via keyboard
183    if sim.state.active_camera_type == "fpv":
184        entity_id = sim.robot_eids[sim.state.fpv_ix]
185        control_input = np.float32([
186            rl.IsKeyDown(rl.KEY_A)       - rl.IsKeyDown(rl.KEY_D),           # left/right (x)
187            rl.IsKeyDown(rl.KEY_PAGE_UP) - rl.IsKeyDown(rl.KEY_PAGE_DOWN),   # up/down (y)
188            rl.IsKeyDown(rl.KEY_W)       - rl.IsKeyDown(rl.KEY_S),           # forward/backward (z)
189            rl.IsKeyDown(rl.KEY_DOWN)    - rl.IsKeyDown(rl.KEY_UP),          # pitch
190            rl.IsKeyDown(rl.KEY_Q)       - rl.IsKeyDown(rl.KEY_E),           # yaw
191            rl.IsKeyDown(rl.KEY_RIGHT)   - rl.IsKeyDown(rl.KEY_LEFT),        # roll
192        ])
193        if (control_input != 0).any():
194            msgs += [{"cmd": "entity_set_data", "entity_id": entity_id, "component": "HasMotionInput",
195                     "data": {"motion_input": control_input}}]
196
197    # misc / other commands
198    if rl.IsKeyPressed(rl.KEY_F9):
199        msgs += [{"cmd": "sim_set_collision_cell_size", "value": np.maximum(sim.state.collision_cell_size - 0.1, 0.1)}]
200    if rl.IsKeyPressed(rl.KEY_F10):
201        msgs += [{"cmd": "sim_set_collision_cell_size", "value": np.minimum(sim.state.collision_cell_size + 0.1, 30)}]
202    if rl.IsKeyPressed(rl.KEY_T):
203        msgs += [{"cmd": "sim_display_uav_trace", "value": not sim.state.display_uav_trace}]
204
205    for msg in msgs:
206        res = _handle_message(sim, Message("script", data=msg), init_config=INIT_CONFIG)
207        if "error" in res:
208            logger.error(res["error"])
209        else:
210            if not (msg["cmd"] == "entity_set_data" and msg.get("component", "") == "HasMotionInput"):
211                logger.debug(res["status"])

Handle keyboard input for the main UI. We create a message in the same protocol of robosim-ncat/global hanlder Note: network handler is in protocol.py definit the entire protocol handlers of protocol.json.

def main(args: argparse.Namespace):
215def main(args: Namespace):
216    """main fn"""
217    global INIT_CONFIG # pylint: disable=global-statement
218    rl.SetConfigFlags(rl.FLAG_WINDOW_HIDDEN | rl.FLAG_WINDOW_UNDECORATED if args.headless else rl.FLAG_WINDOW_RESIZABLE)
219    rl.InitWindow(SCREEN_WIDTH, SCREEN_HEIGHT, b"UAV Trajectory Simulation")
220    rl.SetWindowPosition(20, 200)
221    rl.SetTargetFPS(0)
222    rl.DisableEventWaiting()
223    rl.rlEnableBackfaceCulling()
224
225    if args.map_path is None:
226        world = build_default_scene_objects()
227    else:
228        with open(args.map_path, "r") as fp:
229            world = world_from_dict(json.load(fp)["world"])
230
231    world.update()
232    if len(world.query(HasFPV)) == 0:
233        build_robots(world, args.n_robots, args.robot_models)
234
235    state = SimState(collision_cell_size=make_collision_cell_size(world))
236    conn_manager = ConnectionManager("0.0.0.0", args.port, codec=MsgpackCodec(), max_connections=args.n_robots,
237                                     client_type=RobosimClient)
238
239    with open(args.protocol_path, "r") as fp:
240        protocol = Protocol.from_dict(json.load(fp), max_robot_ix=args.n_robots - 1, components=world.component_names)
241    sim = Simulator(world=world, state=state, connection_manager=conn_manager, plugins=args.plugins, protocol=protocol)
242    logger.info("Created default map" if args.map_path is None else f"Constructed map from '{args.map_path}'")
243    logger.info(f"Created {len(world.query(HasModel, exclude=[HasFPV]))} objects and {len(world.query(HasFPV))} robots")
244    logger.info(f"Created simulator object:\n{sim}")
245    logger.info(f"Raylib rendering on: {' / '.join(rl_get_device_and_renderer())}")
246    INIT_CONFIG = sim.to_dict()
247
248    conn_manager.on_disconnect = sim.client_on_disconnect
249    conn_manager.network_handler = partial(network_handler, sim=sim)
250    conn_manager.start()
251
252    fpv_system = FPVCameraSystem()
253    physics_system = PhysicsSystem()
254
255    clock = Clock(dt=DT, max_ticks=MAX_SUBTICKS_PER_RENDER_TICK)
256    while True:
257        sim.world.update()
258        clock.wait_and_tick()
259
260        # I/O systems: read messages from the keyboard as well as all robots (via plugins/channels)
261        if rl.IsKeyPressed(rl.KEY_ESCAPE) or not conn_manager.is_alive():
262            break
263        sim.world.query(HasMotionInput).motion_input = 0 # reset all the inputs for the physics system
264        keyboard_handler(sim)
265        network_slow_handler(sim, init_config=INIT_CONFIG)
266
267        # Update systems
268        if not args.headless:
269            sim.active_camera.manual_movement()
270        fpv_system(world=sim.world)
271
272        if sim.state.display_uav_trace:
273            robots = sim.world.query(HasFPV, HasPose)
274            for robot_id, pose in zip(robots.entity_ids, robots.pose):
275                robot_traces = sim.state.uav_traces.setdefault(
276                    int(robot_id), FixedSizeDict(maxlen=sim.state.uav_traces_max_len))
277                robot_traces[tuple(pose[0:3, 3].tolist())] = True
278
279        # Physics systems + the plugins callbacks before/after it
280        prev = time.perf_counter()
281        sim.plugins_manager.on_before_physics(sim.world)
282        for _ in clock.drain():
283            physics_system(world=sim.world, dt=DT, cell_size=sim.state.collision_cell_size)
284        sim.plugins_manager.on_after_physics(sim.world)
285        sim.physics_ticks_stats.append(time.perf_counter() - prev)
286
287        # Rendering: first fpv cameras (for each robot), then the main UI camera
288        prev = time.perf_counter()
289        sim.render_fpv_cameras(extra_drawables=sim.plugins_manager.plugins)
290        if not args.headless:
291            sim.render_main_camera(extra_drawables=sim.plugins_manager.plugins)
292        sim.render_ticks_stats.append(time.perf_counter() - prev)
293
294        sim.plugins_manager.responses_handler(world=sim.world)
295
296    rl.CloseWindow()

main fn