From 4fb347183331c8bfb38ce9d39ebb1017ea16d59d Mon Sep 17 00:00:00 2001 From: Syed Daanish Date: Fri, 7 Nov 2025 11:39:23 +0000 Subject: [PATCH] First commit --- .gitignore | 2 + LICENSE | 21 + example_robots/basic_robot.py | 26 + example_robots/keyboard_robot.py | 214 ++++++ readme.html | 647 ++++++++++++++++++ run_simulator.py | 110 +++ scripts/run_comp_match.py | 298 ++++++++ setup.py | 134 ++++ simulator/VERSION | 1 + .../__pycache__/environment.cpython-310.pyc | Bin 0 -> 1151 bytes .../__pycache__/environment.cpython-313.pyc | Bin 0 -> 1698 bytes .../competition_supervisor.py | 263 +++++++ .../lighting_control.py | 287 ++++++++ .../competition_supervisor/runtime.ini | 3 + .../controllers/usercode_runner/runtime.ini | 3 + .../usercode_runner/usercode_runner.py | 150 ++++ simulator/environment.py | 42 ++ .../__pycache__/robot_logging.cpython-313.pyc | Bin 0 -> 5248 bytes .../__pycache__/robot_utils.cpython-313.pyc | Bin 0 -> 5242 bytes simulator/modules/robot_logging.py | 132 ++++ simulator/modules/robot_utils.py | 120 ++++ simulator/modules/sbot_interface/__init__.py | 0 .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 180 bytes .../__pycache__/setup.cpython-313.pyc | Bin 0 -> 4586 bytes .../__pycache__/socket_server.cpython-313.pyc | Bin 0 -> 12308 bytes .../modules/sbot_interface/boards/__init__.py | 22 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 869 bytes .../__pycache__/arduino.cpython-313.pyc | Bin 0 -> 4944 bytes .../boards/__pycache__/camera.cpython-313.pyc | Bin 0 -> 3584 bytes .../__pycache__/led_board.cpython-313.pyc | Bin 0 -> 4900 bytes .../__pycache__/motor_board.cpython-313.pyc | Bin 0 -> 4900 bytes .../__pycache__/power_board.cpython-313.pyc | Bin 0 -> 8315 bytes .../__pycache__/servo_board.cpython-313.pyc | Bin 0 -> 4927 bytes .../__pycache__/time_server.cpython-313.pyc | Bin 0 -> 3187 bytes .../modules/sbot_interface/boards/arduino.py | 112 +++ .../modules/sbot_interface/boards/camera.py | 75 ++ .../sbot_interface/boards/led_board.py | 117 ++++ .../sbot_interface/boards/motor_board.py | 107 +++ .../sbot_interface/boards/power_board.py | 192 ++++++ .../sbot_interface/boards/servo_board.py | 105 +++ .../sbot_interface/boards/time_server.py | 66 ++ .../sbot_interface/devices/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 274 bytes .../arduino_devices.cpython-313.pyc | Bin 0 -> 12571 bytes .../__pycache__/camera.cpython-313.pyc | Bin 0 -> 5152 bytes .../devices/__pycache__/led.cpython-313.pyc | Bin 0 -> 3736 bytes .../devices/__pycache__/motor.cpython-313.pyc | Bin 0 -> 6278 bytes .../devices/__pycache__/power.cpython-313.pyc | Bin 0 -> 7027 bytes .../devices/__pycache__/servo.cpython-313.pyc | Bin 0 -> 6124 bytes .../devices/__pycache__/util.cpython-313.pyc | Bin 0 -> 5472 bytes .../sbot_interface/devices/arduino_devices.py | 275 ++++++++ .../modules/sbot_interface/devices/camera.py | 105 +++ .../modules/sbot_interface/devices/led.py | 88 +++ .../modules/sbot_interface/devices/motor.py | 155 +++++ .../modules/sbot_interface/devices/power.py | 137 ++++ .../modules/sbot_interface/devices/servo.py | 157 +++++ .../modules/sbot_interface/devices/util.py | 159 +++++ simulator/modules/sbot_interface/setup.py | 134 ++++ .../modules/sbot_interface/socket_server.py | 228 ++++++ simulator/protos/SR2025bot.proto | 204 ++++++ simulator/protos/SRObot.proto | 178 +++++ simulator/protos/arena/Arena.proto | 136 ++++ simulator/protos/arena/Deck.proto | 50 ++ simulator/protos/arena/Pillar.proto | 76 ++ simulator/protos/arena/TriangleDeck.proto | 68 ++ simulator/protos/props/BoxToken.proto | 125 ++++ simulator/protos/props/Can.proto | 36 + simulator/protos/props/Marker.proto | 99 +++ simulator/protos/robot/BumpSensor.proto | 28 + simulator/protos/robot/Caster.proto | 52 ++ simulator/protos/robot/Flag.proto | 51 ++ simulator/protos/robot/MotorAssembly.proto | 85 +++ simulator/protos/robot/RGBLed.proto | 37 + .../protos/robot/ReflectanceSensor.proto | 40 ++ simulator/protos/robot/RobotCamera.proto | 56 ++ simulator/protos/robot/UltrasoundModule.proto | 75 ++ simulator/protos/robot/VacuumSucker.proto | 201 ++++++ simulator/worlds/.arena.wbproj | 10 + simulator/worlds/arena.wbt | 289 ++++++++ simulator/worlds/arena_floor.png | Bin 0 -> 154048 bytes simulator/worlds/sim_markers/0.png | Bin 0 -> 3255 bytes simulator/worlds/sim_markers/1.png | Bin 0 -> 3266 bytes simulator/worlds/sim_markers/10.png | Bin 0 -> 3267 bytes simulator/worlds/sim_markers/100.png | Bin 0 -> 3259 bytes simulator/worlds/sim_markers/101.png | Bin 0 -> 3273 bytes simulator/worlds/sim_markers/102.png | Bin 0 -> 3259 bytes simulator/worlds/sim_markers/103.png | Bin 0 -> 3271 bytes simulator/worlds/sim_markers/104.png | Bin 0 -> 3265 bytes simulator/worlds/sim_markers/105.png | Bin 0 -> 3262 bytes simulator/worlds/sim_markers/106.png | Bin 0 -> 3259 bytes simulator/worlds/sim_markers/107.png | Bin 0 -> 3271 bytes simulator/worlds/sim_markers/108.png | Bin 0 -> 3266 bytes simulator/worlds/sim_markers/109.png | Bin 0 -> 3264 bytes simulator/worlds/sim_markers/11.png | Bin 0 -> 3262 bytes simulator/worlds/sim_markers/110.png | Bin 0 -> 3270 bytes simulator/worlds/sim_markers/111.png | Bin 0 -> 3263 bytes simulator/worlds/sim_markers/112.png | Bin 0 -> 3271 bytes simulator/worlds/sim_markers/113.png | Bin 0 -> 3270 bytes simulator/worlds/sim_markers/114.png | Bin 0 -> 3258 bytes simulator/worlds/sim_markers/115.png | Bin 0 -> 3271 bytes simulator/worlds/sim_markers/116.png | Bin 0 -> 3263 bytes simulator/worlds/sim_markers/117.png | Bin 0 -> 3265 bytes simulator/worlds/sim_markers/118.png | Bin 0 -> 3260 bytes simulator/worlds/sim_markers/119.png | Bin 0 -> 3266 bytes simulator/worlds/sim_markers/12.png | Bin 0 -> 3267 bytes simulator/worlds/sim_markers/120.png | Bin 0 -> 3264 bytes simulator/worlds/sim_markers/121.png | Bin 0 -> 3266 bytes simulator/worlds/sim_markers/122.png | Bin 0 -> 3273 bytes simulator/worlds/sim_markers/123.png | Bin 0 -> 3259 bytes simulator/worlds/sim_markers/124.png | Bin 0 -> 3253 bytes simulator/worlds/sim_markers/125.png | Bin 0 -> 3259 bytes simulator/worlds/sim_markers/126.png | Bin 0 -> 3269 bytes simulator/worlds/sim_markers/127.png | Bin 0 -> 3262 bytes simulator/worlds/sim_markers/128.png | Bin 0 -> 3263 bytes simulator/worlds/sim_markers/129.png | Bin 0 -> 3257 bytes simulator/worlds/sim_markers/13.png | Bin 0 -> 3265 bytes simulator/worlds/sim_markers/130.png | Bin 0 -> 3261 bytes simulator/worlds/sim_markers/131.png | Bin 0 -> 3254 bytes simulator/worlds/sim_markers/132.png | Bin 0 -> 3265 bytes simulator/worlds/sim_markers/133.png | Bin 0 -> 3260 bytes simulator/worlds/sim_markers/134.png | Bin 0 -> 3271 bytes simulator/worlds/sim_markers/135.png | Bin 0 -> 3254 bytes simulator/worlds/sim_markers/136.png | Bin 0 -> 3267 bytes simulator/worlds/sim_markers/137.png | Bin 0 -> 3271 bytes simulator/worlds/sim_markers/138.png | Bin 0 -> 3269 bytes simulator/worlds/sim_markers/139.png | Bin 0 -> 3275 bytes simulator/worlds/sim_markers/14.png | Bin 0 -> 3266 bytes simulator/worlds/sim_markers/140.png | Bin 0 -> 3257 bytes simulator/worlds/sim_markers/141.png | Bin 0 -> 3258 bytes simulator/worlds/sim_markers/142.png | Bin 0 -> 3275 bytes simulator/worlds/sim_markers/143.png | Bin 0 -> 3263 bytes simulator/worlds/sim_markers/144.png | Bin 0 -> 3261 bytes simulator/worlds/sim_markers/145.png | Bin 0 -> 3268 bytes simulator/worlds/sim_markers/146.png | Bin 0 -> 3259 bytes simulator/worlds/sim_markers/147.png | Bin 0 -> 3270 bytes simulator/worlds/sim_markers/148.png | Bin 0 -> 3261 bytes simulator/worlds/sim_markers/149.png | Bin 0 -> 3270 bytes simulator/worlds/sim_markers/15.png | Bin 0 -> 3257 bytes simulator/worlds/sim_markers/150.png | Bin 0 -> 3262 bytes simulator/worlds/sim_markers/151.png | Bin 0 -> 3260 bytes simulator/worlds/sim_markers/152.png | Bin 0 -> 3257 bytes simulator/worlds/sim_markers/153.png | Bin 0 -> 3259 bytes simulator/worlds/sim_markers/154.png | Bin 0 -> 3264 bytes simulator/worlds/sim_markers/155.png | Bin 0 -> 3266 bytes simulator/worlds/sim_markers/156.png | Bin 0 -> 3261 bytes simulator/worlds/sim_markers/157.png | Bin 0 -> 3271 bytes simulator/worlds/sim_markers/158.png | Bin 0 -> 3264 bytes simulator/worlds/sim_markers/159.png | Bin 0 -> 3250 bytes simulator/worlds/sim_markers/16.png | Bin 0 -> 3262 bytes simulator/worlds/sim_markers/160.png | Bin 0 -> 3263 bytes simulator/worlds/sim_markers/161.png | Bin 0 -> 3252 bytes simulator/worlds/sim_markers/162.png | Bin 0 -> 3269 bytes simulator/worlds/sim_markers/163.png | Bin 0 -> 3274 bytes simulator/worlds/sim_markers/164.png | Bin 0 -> 3262 bytes simulator/worlds/sim_markers/165.png | Bin 0 -> 3271 bytes simulator/worlds/sim_markers/166.png | Bin 0 -> 3260 bytes simulator/worlds/sim_markers/167.png | Bin 0 -> 3260 bytes simulator/worlds/sim_markers/168.png | Bin 0 -> 3268 bytes simulator/worlds/sim_markers/169.png | Bin 0 -> 3249 bytes simulator/worlds/sim_markers/17.png | Bin 0 -> 3267 bytes simulator/worlds/sim_markers/170.png | Bin 0 -> 3264 bytes simulator/worlds/sim_markers/171.png | Bin 0 -> 3276 bytes simulator/worlds/sim_markers/172.png | Bin 0 -> 3253 bytes simulator/worlds/sim_markers/173.png | Bin 0 -> 3254 bytes simulator/worlds/sim_markers/174.png | Bin 0 -> 3253 bytes simulator/worlds/sim_markers/175.png | Bin 0 -> 3259 bytes simulator/worlds/sim_markers/176.png | Bin 0 -> 3263 bytes simulator/worlds/sim_markers/177.png | Bin 0 -> 3273 bytes simulator/worlds/sim_markers/178.png | Bin 0 -> 3265 bytes simulator/worlds/sim_markers/179.png | Bin 0 -> 3252 bytes simulator/worlds/sim_markers/18.png | Bin 0 -> 3269 bytes simulator/worlds/sim_markers/180.png | Bin 0 -> 3256 bytes simulator/worlds/sim_markers/181.png | Bin 0 -> 3252 bytes simulator/worlds/sim_markers/182.png | Bin 0 -> 3264 bytes simulator/worlds/sim_markers/183.png | Bin 0 -> 3259 bytes simulator/worlds/sim_markers/184.png | Bin 0 -> 3260 bytes simulator/worlds/sim_markers/185.png | Bin 0 -> 3250 bytes simulator/worlds/sim_markers/186.png | Bin 0 -> 3265 bytes simulator/worlds/sim_markers/187.png | Bin 0 -> 3273 bytes simulator/worlds/sim_markers/188.png | Bin 0 -> 3264 bytes simulator/worlds/sim_markers/189.png | Bin 0 -> 3262 bytes simulator/worlds/sim_markers/19.png | Bin 0 -> 3265 bytes simulator/worlds/sim_markers/190.png | Bin 0 -> 3262 bytes simulator/worlds/sim_markers/191.png | Bin 0 -> 3259 bytes simulator/worlds/sim_markers/192.png | Bin 0 -> 3269 bytes simulator/worlds/sim_markers/193.png | Bin 0 -> 3265 bytes simulator/worlds/sim_markers/194.png | Bin 0 -> 3279 bytes simulator/worlds/sim_markers/195.png | Bin 0 -> 3270 bytes simulator/worlds/sim_markers/196.png | Bin 0 -> 3255 bytes simulator/worlds/sim_markers/197.png | Bin 0 -> 3263 bytes simulator/worlds/sim_markers/198.png | Bin 0 -> 3277 bytes simulator/worlds/sim_markers/199.png | Bin 0 -> 3263 bytes simulator/worlds/sim_markers/2.png | Bin 0 -> 3269 bytes simulator/worlds/sim_markers/20.png | Bin 0 -> 3258 bytes simulator/worlds/sim_markers/21.png | Bin 0 -> 3266 bytes simulator/worlds/sim_markers/22.png | Bin 0 -> 3274 bytes simulator/worlds/sim_markers/23.png | Bin 0 -> 3268 bytes simulator/worlds/sim_markers/24.png | Bin 0 -> 3253 bytes simulator/worlds/sim_markers/25.png | Bin 0 -> 3272 bytes simulator/worlds/sim_markers/26.png | Bin 0 -> 3253 bytes simulator/worlds/sim_markers/27.png | Bin 0 -> 3267 bytes simulator/worlds/sim_markers/28.png | Bin 0 -> 3271 bytes simulator/worlds/sim_markers/29.png | Bin 0 -> 3261 bytes simulator/worlds/sim_markers/3.png | Bin 0 -> 3259 bytes simulator/worlds/sim_markers/30.png | Bin 0 -> 3268 bytes simulator/worlds/sim_markers/31.png | Bin 0 -> 3275 bytes simulator/worlds/sim_markers/32.png | Bin 0 -> 3262 bytes simulator/worlds/sim_markers/33.png | Bin 0 -> 3263 bytes simulator/worlds/sim_markers/34.png | Bin 0 -> 3268 bytes simulator/worlds/sim_markers/35.png | Bin 0 -> 3260 bytes simulator/worlds/sim_markers/36.png | Bin 0 -> 3262 bytes simulator/worlds/sim_markers/37.png | Bin 0 -> 3259 bytes simulator/worlds/sim_markers/38.png | Bin 0 -> 3262 bytes simulator/worlds/sim_markers/39.png | Bin 0 -> 3279 bytes simulator/worlds/sim_markers/4.png | Bin 0 -> 3265 bytes simulator/worlds/sim_markers/40.png | Bin 0 -> 3267 bytes simulator/worlds/sim_markers/41.png | Bin 0 -> 3269 bytes simulator/worlds/sim_markers/42.png | Bin 0 -> 3275 bytes simulator/worlds/sim_markers/43.png | Bin 0 -> 3256 bytes simulator/worlds/sim_markers/44.png | Bin 0 -> 3269 bytes simulator/worlds/sim_markers/45.png | Bin 0 -> 3271 bytes simulator/worlds/sim_markers/46.png | Bin 0 -> 3266 bytes simulator/worlds/sim_markers/47.png | Bin 0 -> 3266 bytes simulator/worlds/sim_markers/48.png | Bin 0 -> 3264 bytes simulator/worlds/sim_markers/49.png | Bin 0 -> 3265 bytes simulator/worlds/sim_markers/5.png | Bin 0 -> 3265 bytes simulator/worlds/sim_markers/50.png | Bin 0 -> 3257 bytes simulator/worlds/sim_markers/51.png | Bin 0 -> 3249 bytes simulator/worlds/sim_markers/52.png | Bin 0 -> 3270 bytes simulator/worlds/sim_markers/53.png | Bin 0 -> 3273 bytes simulator/worlds/sim_markers/54.png | Bin 0 -> 3261 bytes simulator/worlds/sim_markers/55.png | Bin 0 -> 3258 bytes simulator/worlds/sim_markers/56.png | Bin 0 -> 3258 bytes simulator/worlds/sim_markers/57.png | Bin 0 -> 3265 bytes simulator/worlds/sim_markers/58.png | Bin 0 -> 3252 bytes simulator/worlds/sim_markers/59.png | Bin 0 -> 3261 bytes simulator/worlds/sim_markers/6.png | Bin 0 -> 3263 bytes simulator/worlds/sim_markers/60.png | Bin 0 -> 3268 bytes simulator/worlds/sim_markers/61.png | Bin 0 -> 3255 bytes simulator/worlds/sim_markers/62.png | Bin 0 -> 3269 bytes simulator/worlds/sim_markers/63.png | Bin 0 -> 3272 bytes simulator/worlds/sim_markers/64.png | Bin 0 -> 3276 bytes simulator/worlds/sim_markers/65.png | Bin 0 -> 3266 bytes simulator/worlds/sim_markers/66.png | Bin 0 -> 3261 bytes simulator/worlds/sim_markers/67.png | Bin 0 -> 3276 bytes simulator/worlds/sim_markers/68.png | Bin 0 -> 3264 bytes simulator/worlds/sim_markers/69.png | Bin 0 -> 3268 bytes simulator/worlds/sim_markers/7.png | Bin 0 -> 3260 bytes simulator/worlds/sim_markers/70.png | Bin 0 -> 3267 bytes simulator/worlds/sim_markers/71.png | Bin 0 -> 3264 bytes simulator/worlds/sim_markers/72.png | Bin 0 -> 3272 bytes simulator/worlds/sim_markers/73.png | Bin 0 -> 3263 bytes simulator/worlds/sim_markers/74.png | Bin 0 -> 3262 bytes simulator/worlds/sim_markers/75.png | Bin 0 -> 3260 bytes simulator/worlds/sim_markers/76.png | Bin 0 -> 3275 bytes simulator/worlds/sim_markers/77.png | Bin 0 -> 3270 bytes simulator/worlds/sim_markers/78.png | Bin 0 -> 3264 bytes simulator/worlds/sim_markers/79.png | Bin 0 -> 3274 bytes simulator/worlds/sim_markers/8.png | Bin 0 -> 3259 bytes simulator/worlds/sim_markers/80.png | Bin 0 -> 3268 bytes simulator/worlds/sim_markers/81.png | Bin 0 -> 3262 bytes simulator/worlds/sim_markers/82.png | Bin 0 -> 3269 bytes simulator/worlds/sim_markers/83.png | Bin 0 -> 3269 bytes simulator/worlds/sim_markers/84.png | Bin 0 -> 3264 bytes simulator/worlds/sim_markers/85.png | Bin 0 -> 3261 bytes simulator/worlds/sim_markers/86.png | Bin 0 -> 3271 bytes simulator/worlds/sim_markers/87.png | Bin 0 -> 3266 bytes simulator/worlds/sim_markers/88.png | Bin 0 -> 3269 bytes simulator/worlds/sim_markers/89.png | Bin 0 -> 3264 bytes simulator/worlds/sim_markers/9.png | Bin 0 -> 3262 bytes simulator/worlds/sim_markers/90.png | Bin 0 -> 3264 bytes simulator/worlds/sim_markers/91.png | Bin 0 -> 3261 bytes simulator/worlds/sim_markers/92.png | Bin 0 -> 3279 bytes simulator/worlds/sim_markers/93.png | Bin 0 -> 3264 bytes simulator/worlds/sim_markers/94.png | Bin 0 -> 3267 bytes simulator/worlds/sim_markers/95.png | Bin 0 -> 3258 bytes simulator/worlds/sim_markers/96.png | Bin 0 -> 3264 bytes simulator/worlds/sim_markers/97.png | Bin 0 -> 3269 bytes simulator/worlds/sim_markers/98.png | Bin 0 -> 3256 bytes simulator/worlds/sim_markers/99.png | Bin 0 -> 3255 bytes zone_0/robot.py | 26 + 281 files changed, 6610 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 example_robots/basic_robot.py create mode 100644 example_robots/keyboard_robot.py create mode 100644 readme.html create mode 100755 run_simulator.py create mode 100755 scripts/run_comp_match.py create mode 100755 setup.py create mode 100644 simulator/VERSION create mode 100644 simulator/__pycache__/environment.cpython-310.pyc create mode 100644 simulator/__pycache__/environment.cpython-313.pyc create mode 100644 simulator/controllers/competition_supervisor/competition_supervisor.py create mode 100644 simulator/controllers/competition_supervisor/lighting_control.py create mode 100644 simulator/controllers/competition_supervisor/runtime.ini create mode 100644 simulator/controllers/usercode_runner/runtime.ini create mode 100644 simulator/controllers/usercode_runner/usercode_runner.py create mode 100644 simulator/environment.py create mode 100644 simulator/modules/__pycache__/robot_logging.cpython-313.pyc create mode 100644 simulator/modules/__pycache__/robot_utils.cpython-313.pyc create mode 100644 simulator/modules/robot_logging.py create mode 100644 simulator/modules/robot_utils.py create mode 100644 simulator/modules/sbot_interface/__init__.py create mode 100644 simulator/modules/sbot_interface/__pycache__/__init__.cpython-313.pyc create mode 100644 simulator/modules/sbot_interface/__pycache__/setup.cpython-313.pyc create mode 100644 simulator/modules/sbot_interface/__pycache__/socket_server.cpython-313.pyc create mode 100644 simulator/modules/sbot_interface/boards/__init__.py create mode 100644 simulator/modules/sbot_interface/boards/__pycache__/__init__.cpython-313.pyc create mode 100644 simulator/modules/sbot_interface/boards/__pycache__/arduino.cpython-313.pyc create mode 100644 simulator/modules/sbot_interface/boards/__pycache__/camera.cpython-313.pyc create mode 100644 simulator/modules/sbot_interface/boards/__pycache__/led_board.cpython-313.pyc create mode 100644 simulator/modules/sbot_interface/boards/__pycache__/motor_board.cpython-313.pyc create mode 100644 simulator/modules/sbot_interface/boards/__pycache__/power_board.cpython-313.pyc create mode 100644 simulator/modules/sbot_interface/boards/__pycache__/servo_board.cpython-313.pyc create mode 100644 simulator/modules/sbot_interface/boards/__pycache__/time_server.cpython-313.pyc create mode 100644 simulator/modules/sbot_interface/boards/arduino.py create mode 100644 simulator/modules/sbot_interface/boards/camera.py create mode 100644 simulator/modules/sbot_interface/boards/led_board.py create mode 100644 simulator/modules/sbot_interface/boards/motor_board.py create mode 100644 simulator/modules/sbot_interface/boards/power_board.py create mode 100644 simulator/modules/sbot_interface/boards/servo_board.py create mode 100644 simulator/modules/sbot_interface/boards/time_server.py create mode 100644 simulator/modules/sbot_interface/devices/__init__.py create mode 100644 simulator/modules/sbot_interface/devices/__pycache__/__init__.cpython-313.pyc create mode 100644 simulator/modules/sbot_interface/devices/__pycache__/arduino_devices.cpython-313.pyc create mode 100644 simulator/modules/sbot_interface/devices/__pycache__/camera.cpython-313.pyc create mode 100644 simulator/modules/sbot_interface/devices/__pycache__/led.cpython-313.pyc create mode 100644 simulator/modules/sbot_interface/devices/__pycache__/motor.cpython-313.pyc create mode 100644 simulator/modules/sbot_interface/devices/__pycache__/power.cpython-313.pyc create mode 100644 simulator/modules/sbot_interface/devices/__pycache__/servo.cpython-313.pyc create mode 100644 simulator/modules/sbot_interface/devices/__pycache__/util.cpython-313.pyc create mode 100644 simulator/modules/sbot_interface/devices/arduino_devices.py create mode 100644 simulator/modules/sbot_interface/devices/camera.py create mode 100644 simulator/modules/sbot_interface/devices/led.py create mode 100644 simulator/modules/sbot_interface/devices/motor.py create mode 100644 simulator/modules/sbot_interface/devices/power.py create mode 100644 simulator/modules/sbot_interface/devices/servo.py create mode 100644 simulator/modules/sbot_interface/devices/util.py create mode 100644 simulator/modules/sbot_interface/setup.py create mode 100644 simulator/modules/sbot_interface/socket_server.py create mode 100755 simulator/protos/SR2025bot.proto create mode 100755 simulator/protos/SRObot.proto create mode 100755 simulator/protos/arena/Arena.proto create mode 100755 simulator/protos/arena/Deck.proto create mode 100755 simulator/protos/arena/Pillar.proto create mode 100755 simulator/protos/arena/TriangleDeck.proto create mode 100755 simulator/protos/props/BoxToken.proto create mode 100755 simulator/protos/props/Can.proto create mode 100755 simulator/protos/props/Marker.proto create mode 100755 simulator/protos/robot/BumpSensor.proto create mode 100755 simulator/protos/robot/Caster.proto create mode 100755 simulator/protos/robot/Flag.proto create mode 100755 simulator/protos/robot/MotorAssembly.proto create mode 100755 simulator/protos/robot/RGBLed.proto create mode 100755 simulator/protos/robot/ReflectanceSensor.proto create mode 100755 simulator/protos/robot/RobotCamera.proto create mode 100755 simulator/protos/robot/UltrasoundModule.proto create mode 100644 simulator/protos/robot/VacuumSucker.proto create mode 100644 simulator/worlds/.arena.wbproj create mode 100755 simulator/worlds/arena.wbt create mode 100644 simulator/worlds/arena_floor.png create mode 100644 simulator/worlds/sim_markers/0.png create mode 100644 simulator/worlds/sim_markers/1.png create mode 100644 simulator/worlds/sim_markers/10.png create mode 100644 simulator/worlds/sim_markers/100.png create mode 100644 simulator/worlds/sim_markers/101.png create mode 100644 simulator/worlds/sim_markers/102.png create mode 100644 simulator/worlds/sim_markers/103.png create mode 100644 simulator/worlds/sim_markers/104.png create mode 100644 simulator/worlds/sim_markers/105.png create mode 100644 simulator/worlds/sim_markers/106.png create mode 100644 simulator/worlds/sim_markers/107.png create mode 100644 simulator/worlds/sim_markers/108.png create mode 100644 simulator/worlds/sim_markers/109.png create mode 100644 simulator/worlds/sim_markers/11.png create mode 100644 simulator/worlds/sim_markers/110.png create mode 100644 simulator/worlds/sim_markers/111.png create mode 100644 simulator/worlds/sim_markers/112.png create mode 100644 simulator/worlds/sim_markers/113.png create mode 100644 simulator/worlds/sim_markers/114.png create mode 100644 simulator/worlds/sim_markers/115.png create mode 100644 simulator/worlds/sim_markers/116.png create mode 100644 simulator/worlds/sim_markers/117.png create mode 100644 simulator/worlds/sim_markers/118.png create mode 100644 simulator/worlds/sim_markers/119.png create mode 100644 simulator/worlds/sim_markers/12.png create mode 100644 simulator/worlds/sim_markers/120.png create mode 100644 simulator/worlds/sim_markers/121.png create mode 100644 simulator/worlds/sim_markers/122.png create mode 100644 simulator/worlds/sim_markers/123.png create mode 100644 simulator/worlds/sim_markers/124.png create mode 100644 simulator/worlds/sim_markers/125.png create mode 100644 simulator/worlds/sim_markers/126.png create mode 100644 simulator/worlds/sim_markers/127.png create mode 100644 simulator/worlds/sim_markers/128.png create mode 100644 simulator/worlds/sim_markers/129.png create mode 100644 simulator/worlds/sim_markers/13.png create mode 100644 simulator/worlds/sim_markers/130.png create mode 100644 simulator/worlds/sim_markers/131.png create mode 100644 simulator/worlds/sim_markers/132.png create mode 100644 simulator/worlds/sim_markers/133.png create mode 100644 simulator/worlds/sim_markers/134.png create mode 100644 simulator/worlds/sim_markers/135.png create mode 100644 simulator/worlds/sim_markers/136.png create mode 100644 simulator/worlds/sim_markers/137.png create mode 100644 simulator/worlds/sim_markers/138.png create mode 100644 simulator/worlds/sim_markers/139.png create mode 100644 simulator/worlds/sim_markers/14.png create mode 100644 simulator/worlds/sim_markers/140.png create mode 100644 simulator/worlds/sim_markers/141.png create mode 100644 simulator/worlds/sim_markers/142.png create mode 100644 simulator/worlds/sim_markers/143.png create mode 100644 simulator/worlds/sim_markers/144.png create mode 100644 simulator/worlds/sim_markers/145.png create mode 100644 simulator/worlds/sim_markers/146.png create mode 100644 simulator/worlds/sim_markers/147.png create mode 100644 simulator/worlds/sim_markers/148.png create mode 100644 simulator/worlds/sim_markers/149.png create mode 100644 simulator/worlds/sim_markers/15.png create mode 100644 simulator/worlds/sim_markers/150.png create mode 100644 simulator/worlds/sim_markers/151.png create mode 100644 simulator/worlds/sim_markers/152.png create mode 100644 simulator/worlds/sim_markers/153.png create mode 100644 simulator/worlds/sim_markers/154.png create mode 100644 simulator/worlds/sim_markers/155.png create mode 100644 simulator/worlds/sim_markers/156.png create mode 100644 simulator/worlds/sim_markers/157.png create mode 100644 simulator/worlds/sim_markers/158.png create mode 100644 simulator/worlds/sim_markers/159.png create mode 100644 simulator/worlds/sim_markers/16.png create mode 100644 simulator/worlds/sim_markers/160.png create mode 100644 simulator/worlds/sim_markers/161.png create mode 100644 simulator/worlds/sim_markers/162.png create mode 100644 simulator/worlds/sim_markers/163.png create mode 100644 simulator/worlds/sim_markers/164.png create mode 100644 simulator/worlds/sim_markers/165.png create mode 100644 simulator/worlds/sim_markers/166.png create mode 100644 simulator/worlds/sim_markers/167.png create mode 100644 simulator/worlds/sim_markers/168.png create mode 100644 simulator/worlds/sim_markers/169.png create mode 100644 simulator/worlds/sim_markers/17.png create mode 100644 simulator/worlds/sim_markers/170.png create mode 100644 simulator/worlds/sim_markers/171.png create mode 100644 simulator/worlds/sim_markers/172.png create mode 100644 simulator/worlds/sim_markers/173.png create mode 100644 simulator/worlds/sim_markers/174.png create mode 100644 simulator/worlds/sim_markers/175.png create mode 100644 simulator/worlds/sim_markers/176.png create mode 100644 simulator/worlds/sim_markers/177.png create mode 100644 simulator/worlds/sim_markers/178.png create mode 100644 simulator/worlds/sim_markers/179.png create mode 100644 simulator/worlds/sim_markers/18.png create mode 100644 simulator/worlds/sim_markers/180.png create mode 100644 simulator/worlds/sim_markers/181.png create mode 100644 simulator/worlds/sim_markers/182.png create mode 100644 simulator/worlds/sim_markers/183.png create mode 100644 simulator/worlds/sim_markers/184.png create mode 100644 simulator/worlds/sim_markers/185.png create mode 100644 simulator/worlds/sim_markers/186.png create mode 100644 simulator/worlds/sim_markers/187.png create mode 100644 simulator/worlds/sim_markers/188.png create mode 100644 simulator/worlds/sim_markers/189.png create mode 100644 simulator/worlds/sim_markers/19.png create mode 100644 simulator/worlds/sim_markers/190.png create mode 100644 simulator/worlds/sim_markers/191.png create mode 100644 simulator/worlds/sim_markers/192.png create mode 100644 simulator/worlds/sim_markers/193.png create mode 100644 simulator/worlds/sim_markers/194.png create mode 100644 simulator/worlds/sim_markers/195.png create mode 100644 simulator/worlds/sim_markers/196.png create mode 100644 simulator/worlds/sim_markers/197.png create mode 100644 simulator/worlds/sim_markers/198.png create mode 100644 simulator/worlds/sim_markers/199.png create mode 100644 simulator/worlds/sim_markers/2.png create mode 100644 simulator/worlds/sim_markers/20.png create mode 100644 simulator/worlds/sim_markers/21.png create mode 100644 simulator/worlds/sim_markers/22.png create mode 100644 simulator/worlds/sim_markers/23.png create mode 100644 simulator/worlds/sim_markers/24.png create mode 100644 simulator/worlds/sim_markers/25.png create mode 100644 simulator/worlds/sim_markers/26.png create mode 100644 simulator/worlds/sim_markers/27.png create mode 100644 simulator/worlds/sim_markers/28.png create mode 100644 simulator/worlds/sim_markers/29.png create mode 100644 simulator/worlds/sim_markers/3.png create mode 100644 simulator/worlds/sim_markers/30.png create mode 100644 simulator/worlds/sim_markers/31.png create mode 100644 simulator/worlds/sim_markers/32.png create mode 100644 simulator/worlds/sim_markers/33.png create mode 100644 simulator/worlds/sim_markers/34.png create mode 100644 simulator/worlds/sim_markers/35.png create mode 100644 simulator/worlds/sim_markers/36.png create mode 100644 simulator/worlds/sim_markers/37.png create mode 100644 simulator/worlds/sim_markers/38.png create mode 100644 simulator/worlds/sim_markers/39.png create mode 100644 simulator/worlds/sim_markers/4.png create mode 100644 simulator/worlds/sim_markers/40.png create mode 100644 simulator/worlds/sim_markers/41.png create mode 100644 simulator/worlds/sim_markers/42.png create mode 100644 simulator/worlds/sim_markers/43.png create mode 100644 simulator/worlds/sim_markers/44.png create mode 100644 simulator/worlds/sim_markers/45.png create mode 100644 simulator/worlds/sim_markers/46.png create mode 100644 simulator/worlds/sim_markers/47.png create mode 100644 simulator/worlds/sim_markers/48.png create mode 100644 simulator/worlds/sim_markers/49.png create mode 100644 simulator/worlds/sim_markers/5.png create mode 100644 simulator/worlds/sim_markers/50.png create mode 100644 simulator/worlds/sim_markers/51.png create mode 100644 simulator/worlds/sim_markers/52.png create mode 100644 simulator/worlds/sim_markers/53.png create mode 100644 simulator/worlds/sim_markers/54.png create mode 100644 simulator/worlds/sim_markers/55.png create mode 100644 simulator/worlds/sim_markers/56.png create mode 100644 simulator/worlds/sim_markers/57.png create mode 100644 simulator/worlds/sim_markers/58.png create mode 100644 simulator/worlds/sim_markers/59.png create mode 100644 simulator/worlds/sim_markers/6.png create mode 100644 simulator/worlds/sim_markers/60.png create mode 100644 simulator/worlds/sim_markers/61.png create mode 100644 simulator/worlds/sim_markers/62.png create mode 100644 simulator/worlds/sim_markers/63.png create mode 100644 simulator/worlds/sim_markers/64.png create mode 100644 simulator/worlds/sim_markers/65.png create mode 100644 simulator/worlds/sim_markers/66.png create mode 100644 simulator/worlds/sim_markers/67.png create mode 100644 simulator/worlds/sim_markers/68.png create mode 100644 simulator/worlds/sim_markers/69.png create mode 100644 simulator/worlds/sim_markers/7.png create mode 100644 simulator/worlds/sim_markers/70.png create mode 100644 simulator/worlds/sim_markers/71.png create mode 100644 simulator/worlds/sim_markers/72.png create mode 100644 simulator/worlds/sim_markers/73.png create mode 100644 simulator/worlds/sim_markers/74.png create mode 100644 simulator/worlds/sim_markers/75.png create mode 100644 simulator/worlds/sim_markers/76.png create mode 100644 simulator/worlds/sim_markers/77.png create mode 100644 simulator/worlds/sim_markers/78.png create mode 100644 simulator/worlds/sim_markers/79.png create mode 100644 simulator/worlds/sim_markers/8.png create mode 100644 simulator/worlds/sim_markers/80.png create mode 100644 simulator/worlds/sim_markers/81.png create mode 100644 simulator/worlds/sim_markers/82.png create mode 100644 simulator/worlds/sim_markers/83.png create mode 100644 simulator/worlds/sim_markers/84.png create mode 100644 simulator/worlds/sim_markers/85.png create mode 100644 simulator/worlds/sim_markers/86.png create mode 100644 simulator/worlds/sim_markers/87.png create mode 100644 simulator/worlds/sim_markers/88.png create mode 100644 simulator/worlds/sim_markers/89.png create mode 100644 simulator/worlds/sim_markers/9.png create mode 100644 simulator/worlds/sim_markers/90.png create mode 100644 simulator/worlds/sim_markers/91.png create mode 100644 simulator/worlds/sim_markers/92.png create mode 100644 simulator/worlds/sim_markers/93.png create mode 100644 simulator/worlds/sim_markers/94.png create mode 100644 simulator/worlds/sim_markers/95.png create mode 100644 simulator/worlds/sim_markers/96.png create mode 100644 simulator/worlds/sim_markers/97.png create mode 100644 simulator/worlds/sim_markers/98.png create mode 100644 simulator/worlds/sim_markers/99.png create mode 100644 zone_0/robot.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..81fa003 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.txt +venv/* diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7e6b055 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 SourceBots + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/example_robots/basic_robot.py b/example_robots/basic_robot.py new file mode 100644 index 0000000..6e791eb --- /dev/null +++ b/example_robots/basic_robot.py @@ -0,0 +1,26 @@ +from sr.robot3 import Robot + +robot = Robot() + +robot.motor_board.motors[0].power = 1 +robot.motor_board.motors[1].power = 1 + +# measure the distance of the right ultrasound sensor +# pin 6 is the trigger pin, pin 7 is the echo pin +distance = robot.arduino.ultrasound_measure(6, 7) +print(f"Right ultrasound distance: {distance / 1000} meters") + +# motor board, channel 0 to half power forward +robot.motor_board.motors[0].power = 0.5 + +# motor board, channel 1 to half power forward, +robot.motor_board.motors[1].power = 0.5 +# minimal time has passed at this point, +# so the robot will appear to move forward instead of turning + +# sleep for 2 second +robot.sleep(2) + +# stop both motors +robot.motor_board.motors[0].power = 0 +robot.motor_board.motors[1].power = 0 diff --git a/example_robots/keyboard_robot.py b/example_robots/keyboard_robot.py new file mode 100644 index 0000000..a013942 --- /dev/null +++ b/example_robots/keyboard_robot.py @@ -0,0 +1,214 @@ +import math + +from controller import Keyboard +from sr.robot3 import A0, A1, A2, OUT_H0, Colour, Robot + +# Keyboard sampling period in milliseconds +KEYBOARD_SAMPLING_PERIOD = 100 +NO_KEY_PRESSED = -1 + +CONTROLS = { + "forward": (ord("W"), ord("I")), + "reverse": (ord("S"), ord("K")), + "left": (ord("A"), ord("J")), + "right": (ord("D"), ord("L")), + "sense": (ord("Q"), ord("U")), + "see": (ord("E"), ord("O")), + "led": (ord("Z"), ord("M")), + "sucker_enable": (ord("X"), ord(",")), + "sucker_disable": (ord("C"), ord(".")), + "lift_up": (ord("R"), ord("P")), + "lift_down": (ord("F"), ord(";")), + "boost": (Keyboard.SHIFT, Keyboard.CONTROL), + "angle_unit": (ord("B"), ord("B")), +} + +USE_DEGREES = False + + +class KeyboardInterface: + def __init__(self): + self.keyboard = Keyboard() + self.keyboard.enable(KEYBOARD_SAMPLING_PERIOD) + self.pressed_keys = set() + + def process_keys(self): + new_keys = set() + key = self.keyboard.getKey() + + while key != NO_KEY_PRESSED: + key_ascii = key & 0x7F # mask out modifier keys + key_mod = key & (~0x7F) + + new_keys.add(key_ascii) + if key_mod: + new_keys.add(key_mod) + + key = self.keyboard.getKey() + + key_summary = { + "pressed": new_keys - self.pressed_keys, + "held": new_keys, + "released": self.pressed_keys - new_keys, + } + + self.pressed_keys = new_keys + + return key_summary + + +def angle_str(angle: float) -> str: + if USE_DEGREES: + degrees = math.degrees(angle) + return f"{degrees:.1f}°" + else: + return f"{angle:.4f} rad" + + +def print_sensors(robot: Robot) -> None: + ultrasonic_sensor_names = { + (2, 3): "Front", + (4, 5): "Left", + (6, 7): "Right", + (8, 9): "Back", + } + reflectance_sensor_names = { + A0: "Left", + A1: "Center", + A2: "Right", + } + touch_sensor_names = { + 10: "Front Left", + 11: "Front Right", + 12: "Rear Left", + 13: "Rear Right", + } + + print("Distance sensor readings:") + for (trigger_pin, echo_pin), name in ultrasonic_sensor_names.items(): + dist = robot.arduino.ultrasound_measure(trigger_pin, echo_pin) + print(f"({trigger_pin}, {echo_pin}) {name: <12}: {dist:.0f} mm") + + print("Touch sensor readings:") + for pin, name in touch_sensor_names.items(): + touching = robot.arduino.pins[pin].digital_read() + print(f"{pin} {name: <6}: {touching}") + + print("Reflectance sensor readings:") + for Apin, name in reflectance_sensor_names.items(): + reflectance = robot.arduino.pins[Apin].analog_read() + print(f"{Apin} {name: <12}: {reflectance:.2f} V") + + +def print_camera_detection(robot: Robot) -> None: + markers = robot.camera.see() + if markers: + print(f"Found {len(markers)} makers:") + for marker in markers: + print(f" #{marker.id}") + print( + f" Position: {marker.position.distance:.0f} mm, " + f"{angle_str(marker.position.horizontal_angle)} right, " + f"{angle_str(marker.position.vertical_angle)} up", + ) + yaw, pitch, roll = marker.orientation + print( + f" Orientation: yaw: {angle_str(yaw)}, pitch: {angle_str(pitch)}, " + f"roll: {angle_str(roll)}", + ) + print() + else: + print("No markers") + + print() + + +robot = Robot() +keyboard = KeyboardInterface() +lift_height = robot.servo_board.servos[0].position + +# Automatically set the zone controls based on the robot's zone +# Alternatively, you can set this manually +# ZONE_CONTROLS = 0 +ZONE_CONTROLS = robot.zone + +assert ZONE_CONTROLS < len(CONTROLS["forward"]), \ + "No controls defined for this zone, alter the ZONE_CONTROLS variable to use in this zone." + +print( + "Note: you need to click on 3D viewport for keyboard events to be picked " + "up by webots", +) + +while True: + boost = False + left_power = 0.0 + right_power = 0.0 + + keys = keyboard.process_keys() + + # Actions that are run continuously while the key is held + if CONTROLS["forward"][ZONE_CONTROLS] in keys["held"]: + left_power += 0.5 + right_power += 0.5 + + if CONTROLS["reverse"][ZONE_CONTROLS] in keys["held"]: + left_power += -0.5 + right_power += -0.5 + + if CONTROLS["left"][ZONE_CONTROLS] in keys["held"]: + left_power -= 0.25 + right_power += 0.25 + + if CONTROLS["right"][ZONE_CONTROLS] in keys["held"]: + left_power += 0.25 + right_power -= 0.25 + + if CONTROLS["boost"][ZONE_CONTROLS] in keys["held"]: + boost = True + + if CONTROLS["lift_up"][ZONE_CONTROLS] in keys["held"]: + # constrain to [-1, 1] + lift_height = max(min(lift_height + 0.05, 1), -1) + robot.servo_board.servos[0].position = lift_height + + if CONTROLS["lift_down"][ZONE_CONTROLS] in keys["held"]: + # constrain to [-1, 1] + lift_height = max(min(lift_height - 0.05, 1), -1) + robot.servo_board.servos[0].position = lift_height + + # Actions that are run once when the key is pressed + if CONTROLS["sense"][ZONE_CONTROLS] in keys["pressed"]: + print_sensors(robot) + + if CONTROLS["see"][ZONE_CONTROLS] in keys["pressed"]: + print_camera_detection(robot) + + if CONTROLS["sucker_enable"][ZONE_CONTROLS] in keys["pressed"]: + robot.power_board.outputs[OUT_H0].is_enabled = 1 + + if CONTROLS["sucker_disable"][ZONE_CONTROLS] in keys["pressed"]: + robot.power_board.outputs[OUT_H0].is_enabled = 0 + + if CONTROLS["led"][ZONE_CONTROLS] in keys["pressed"]: + robot.kch.leds[0].colour = Colour.MAGENTA + robot.kch.leds[1].colour = Colour.MAGENTA + robot.kch.leds[2].colour = Colour.MAGENTA + elif CONTROLS["led"][ZONE_CONTROLS] in keys["released"]: + robot.kch.leds[0].colour = Colour.OFF + robot.kch.leds[1].colour = Colour.OFF + robot.kch.leds[2].colour = Colour.OFF + + if CONTROLS["angle_unit"][ZONE_CONTROLS] in keys["pressed"]: + USE_DEGREES = not USE_DEGREES + print(f"Angle unit set to {'degrees' if USE_DEGREES else 'radians'}") + + if boost: + # double power values but constrain to [-1, 1] + left_power = max(min(left_power * 2, 1), -1) + right_power = max(min(right_power * 2, 1), -1) + + robot.motor_board.motors[0].power = left_power + robot.motor_board.motors[1].power = right_power + + robot.sleep(KEYBOARD_SAMPLING_PERIOD / 1000) diff --git a/readme.html b/readme.html new file mode 100644 index 0000000..e6ce372 --- /dev/null +++ b/readme.html @@ -0,0 +1,647 @@ + + + + + + + "title" + + + +
+

The sbot Simulator

+
+ +
+

The robotics simulator for sr-robot3 used by Student Robotics, +powered by Webots. This simulator was originally developed by the Southampton Robotics Outreach +society for the Smallpeice competition and has been adapted for use +by Student Robotics.

+

The simulator is a useful development tool that allows you to become +familiar with our API and test out robotics concepts even if you haven't +finished building your robot yet.

+

The simulator is built around the Webots platform, which runs +the simulation. You control the virtual robot using the sr-robot3 API, just like +on the physical robots.

+

Setup

+

In order to use the simulator a few set-up steps need to be done. +First you need to install Python 3.9+ and Webots R2025a.

+

To install Python, you can download the latest version from the Python website. If you have +already installed Python from a package manager, such as homebrew on +MacOS, apt on Ubuntu, or the Windows store on Windows, you can skip this +step. python download site

+

To install Webots, you can download the latest version from the Webots website. Use the +default settings when installing Webots. webots download site

+

Once you have installed these, for the rest of this guide you will +want to start from the folder have you extracted this simulator +into.

+

The contents of the folder should look like this: File contents of a release

+ +
+
Note:
+
+If you had previously downloaded the simulator, you can copy your code +from the previous installation by copying just the zone_0 +folder from the old installation to the new one. +
+
+

Setting up the Environment

+

Now that you have downloaded and extracted the simulator, you need to +set up the environment to run the simulator. Since the simulator uses +the sr-robot3 library, there are a series of python packages that need +to be installed and Webots needs to be configured to use the correct +version of Python. We have provided a script that will set up this +environment for you.

+

Starting in the folder you extracted the simulator into, run the +script called `setup.py and it will set up the environment for you. A +terminal window will open and you will see the output of the script, if +there are any errors displayed check the Troubleshooting section for help.

+
+
Note:
+
+In order to run the Python script, instead of opening the file you may +need to right-click and select Open with → +Python. Open with Python +
+
Note:
+
+On recent versions of macOS you may need to give Python permission to +access the directory where you have extracted the simulation files. +
+
+

This will create a contained python installation with the required +libraries in a venv folder, this is called a virtual +environment. This also configures the Webots settings to use the correct +version of Python.

+

Opening the Arena

+

Before you can start using the simulator, you need to have followed +the steps in the Setting up the +Simulator section.

+

To open the arena, you need to start in the folder you extracted the +simulator during setup and run the run_simulator.py script. +This will open Webots with the arena loaded and ready to run your +code.

+
+
Note:
+
+In order to run the Python script, instead of opening the file you may +need to right-click and select Open with → +Python. Open with Python +
+
Note:
+
+On recent versions of macOS you may need to give Webots permission to +access the directory where you have extracted the simulation files. +
+
+

The Webots Interface

+

After opening the arena, you shoudl see a window similar to the one +below. This is the Webots interface, which has 5 key areas:

+
    +
  1. The 3D world where the simulation takes place. +
      +
    1. The camera overlay, which shows the images from the robot's +camera.
    2. +
  2. +
  3. The time controls, which allow you to control the speed of the +simulation.
  4. +
  5. The console, which shows logs from your code.
  6. +
  7. The "Scene Tree" which shows the objects in the simulation.
  8. +
  9. The built-in text editor, which allows you to edit your code. We +recommend using an external editor instead to make use of features like +syntax highlighting and code completion.
  10. +
+

Webots Interface

+

The Camera Overlay

+

The camera overlay shows the images from the robot's camera. This +only updates when robot.camera.see() is called in your +code.

+

This image is the raw image that the robot sees, and is not processed +in any way. To see the processed image, you have to save the images to a +file just like on the physical robot.

+

If the camera overlay is closed and you want to get it back, you can +use the Reopening the Camera +Overlay instructions.

+

Time Controls

+

In the simulator, time advances only at the pace that the simulator +is run. The relation between this time and the real passage of time +depends on a couple of factors: the speed the simulation is configured +to run at and the ability of the computer running the simulation to +process it fast enough.

+

You can configure and observe the speed the simulator is running at +from the toolbar in webots:

+

Time Controls

+

From left to right, this has the following controls:

+ +

The Console

+

The console at the bottom of the screen shows all the logs produced +by Webots which includes logs from your code.

+

Where your code logs are are printed they are prefixed with the zone +number and simulation time. Lines are displayed in red if they were +printed to standard error instead of standard output.

+

There are also some Webots error messages that you may see, such +as:

+
+
WARNING: The current physics step could not be computed +correctly.
+
+This is because the simulator is having to slightly move objects to +avoid them intersecting with each other. Generally this can be safely +ignored. +
+
Forced termination (because process didn't terminate itself +after 1 second).
+
+This is because the runner for your code didn't exit by itself when the +simulation ended. Generally this can be safely ignored. +
+
+

Developing Your Code

+

Now that you have the simulator set up, you can start developing your +code.

+

In the folder where you extracted the simulator, you should have a +folder called zone_0. In this folder, you should have a +file called robot.py. This is the code that will be run in +the simulator.

+

The API for the simulator is the same as the API for the physical +robot, so you can use largely the same code in both environments. Check +out the simulated robot section for +information on where the sensors and motors are located on the +robot.

+
+
Note:
+
+When you make changes to your code, you need to save the file and then +reload the world in Webots to see the changes. See the Time Controls section for how to reload the +world. +
+
+

As well as the logs being displayed in the console, they are also +saved to a file. This file is saved in the zone_0 folder +and has a name in the format +log-zone-<zone>-<date>.log. The date is when +that simulation was run.

+

Simulation of Time

+

To allow the simulation to be run at various speeds, +time.sleep must not be used. Instead, +robot.sleep should be used. This allows the simulator to +simulate the time your robot would be sleeping for.

+

While the simulator does simulate the time taken for each call to the +API, it does not simulate the time taken for general computation. This +means that if you have a loop that does not contain a +robot.sleep, the simulator will freeze as it waits for the +loop to complete. If you find the timer is not advancing, or is very +slow, you likely have a loop without a sleep. Generally, it is best +practice to have a robot.sleep in every loop, even if it is +a very short time.

+

The Simulated Robot

+

The simulator contains a pre-built robot model that can be controlled +using the sr-robot3 library. The robot is a differential drive robot +with a camera and a variety of sensors.

+

Simulated Robot

+

Attached Boards

+

The robot has a number of boards attached to it that can be +interacted with using the sr-robot3 library. These boards include:

+ +

Attached Sensors

+
+
Note:
+
+The simulated sensors are not perfectly accurate and have artificial +noise included to more closely reflect reality. +
+
+

All sensors are attached to the Arduino board and can be accessed +using the Arduino API.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SensorConnected PinDescription
Front Ultrasound SensorTrigger: 2
Echo: 3
Measures distance from the front of the robot
Left Ultrasound SensorTrigger: 4
Echo: 5
Measures distance from the left of the robot
Right Ultrasound SensorTrigger: 6
Echo: 7
Measures distance from the right of the robot
Back Ultrasound SensorTrigger: 8
Echo: 9
Measures distance from the back of the robot
Front Left Microswitch10Detects if the front left of the robot has bumped into +something
Front Right Microswitch11Detects if the front right of the robot has bumped into +something
Rear Left Microswitch12Detects if the rear left of the robot has bumped into something
Rear Right Microswitch13Detects if the rear right of the robot has bumped into +something
Left Reflectance SensorA0Measures the reflectance of the surface under the left side of the +robot
Center Reflectance SensorA1Measures the reflectance of the surface under the center of the +robot
Right Reflectance SensorA2Measures the reflectance of the surface under the right side of the +robot
+

Ultrasound Sensors

+

These are the simulated version of ultrasound +sensors.

+

They return the distance to the nearest object in front of the sensor +in a narrow cone of view. Objects beyond 4 meters are not detected.

+

The robot has four ultrasound sensors attached to it, one on each +side of the robot. They can all be accessed using the Arduino API's +ultrasound interface.

+
+
Note:
+
+Since these sensors rely on echoes being reflected back from objects, if +the angle of incidence between the sensor's pulse and the contacted +surface exceeds 22.5 degrees then the sensor will be unable to detect +the object. +
+
+

These appear as a blue board with two silver cylinders on the robot +model. The returned distance is measured from the blue board in the +direction of the silver cylinders.

+

Ultrasound Sensors

+

Reflectance Sensors

+

Across the bottom of the robot, there are three reflectance sensors +that can detect differences in the colour of the surface under the +robot. This is achieved by returning the relative red content of the +surface directly below the sensor.

+

The measured values can then be read using the Analog Input +interface. This is returned as a voltage between 0 and 5 volts, with +lower values indicating a darker surface.

+

These appear as blue rectangles on the robot model.

+

Reflectance Sensors

+

Microswitches

+

On the front and back of the robot, there are microswitches that can +detect if the robot has bumped into something. These appear as red +cuboids on the robot model.

+

The attached pin will read True if the cuboid has +intersected with any other object in the simulation.

+

LEDs

+

LEDs

+

The three rectangles on the back of the robot can have their colours +set using the robot.leds interface.

+

Troubleshooting

+

There are a few common issues that you may encounter when setting up +the simulator. You may receive a warning about your computer's GPU not +being good enough, which can be ignored.

+

If you see a message saying that Python cannot be found that looks +similar to the image below, you need to rerun the setup script and check +if there are any errors displayed.

+

Python not found

+

As well as the guidance above, there are a few other points to note +when using the simulator. These can help you to understand what is +happening and how to get the most out of the simulator.

+

Using Other Zones

+

If the arena has multiple starting zones, you can run multiple robots +in the simulator. To test how your robot behaves in each starting zone +of the arena, you can copy your robot's code to run in each corner.

+

In the folder where you extracted the simulator, alongside the +zone_0 folder, you may have other +zone_<number> folders. Such as zone_1, +zone_2, etc. Each of these folders can contain a +robot.py file that will be run in the corresponding +starting zone of the arena.

+

Performance Optimisations

+

The default settings work for most users however if you are using a +less powerful computer or one without a dedicated graphics card (as is +the case on many laptops), you may wish to adjust the graphics settings +to enable the simulation to run faster.

+

If you find that the simulation runs very slowly we suggest disabling +Anti-Aliasing, Ambient Occlusion and Shadows. These should not affect +the behaviour of the simulation, only the rendered visuals.

+

To do this, open Webots and go to the menu Tools → +Preferences, then select the OpenGL +tab. On this tab, set Ambient Occlusion to "Disabled" +and check the boxes next to "Disable shadows" and "Disable +anti-aliasing".

+

On macOS, Preferences is under the +Webots menu instead of Tools.

+

Preferences Location Preferences Interface

+

Reopening the Camera Overlay

+

To reopen the camera overlay if it has been closed, you can follow +these steps:

+ +

This menu can be seen in the image below. Camera Overlay menu

+

Once visible, the camera overlay can be resized by dragging the +bottom right corner of the overlay. The overlay can also be moved around +the screen by clicking and dragging on the window.

+

Viewing Sensor Paths

+

In addition to the camera overlay, Webots also supports displaying +some of the geometric information that is used to simulate the robot's +sensors. This can be useful for debugging and understanding how the +robot is interacting with the world.

+

To view the field of view of the camera, you can enable the option by +going to ViewOptional Rendering → +Show Camera Frustums. This will show the area that the +camera can see in the 3D world.

+

To view the paths of the ultrasound sensors, you can enable the +option by going to ViewOptional +RenderingShow DistanceSensor Rays.

+

This menu can be seen in the image below. Optional Rendering menu

+

Select the option again to disable the display of the sensor +paths.

+ + diff --git a/run_simulator.py b/run_simulator.py new file mode 100755 index 0000000..fe311fc --- /dev/null +++ b/run_simulator.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +""" +A script to run the project in Webots. + +Largely just a shortcut to running the arena world in Webots. +""" +# ruff: noqa: E501 +from __future__ import annotations + +import sys +import traceback +from os.path import expandvars +from pathlib import Path +from shutil import which +from subprocess import Popen + +BOLD_RED = '\x1b[31;1m' +RESET_COLOUR = '\x1b[0m' + + +if sys.platform == "win32": + from subprocess import CREATE_NEW_PROCESS_GROUP, DETACHED_PROCESS + +if (Path(__file__).parent / 'simulator/VERSION').exists(): + print("Running in release mode") + SIM_BASE = Path(__file__).parent.resolve() +else: + print("Running in development mode") + # Assume the script is in the scripts directory + SIM_BASE = Path(__file__).parents[1].resolve() + +POSSIBLE_WEBOTS_PATHS = [ + ("darwin", "/Applications/Webots.app/Contents/MacOS/webots"), + ("win32", "C:\\Program Files\\Webots\\msys64\\mingw64\\bin\\webotsw.exe"), + ("win32", expandvars("%LOCALAPPDATA%\\Programs\\Webots\\msys64\\mingw64\\bin\\webotsw.exe")), + # Attempt to use the start menu shortcut + ("win32", expandvars("%ProgramData%\\Microsoft\\Windows\\Start Menu\\Programs\\Cyberbotics\\Webots.lnk")), + ("win32", expandvars("%APPDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Cyberbotics\\Webots.lnk")), + ("linux", "/usr/local/bin/webots"), + ("linux", "/usr/bin/webots"), +] + + +def get_webots_parameters() -> tuple[Path, Path]: + """ + Get the paths to the Webots executable and the arena world file. + + :return: The paths to the Webots executable and the arena world file + """ + world_file = SIM_BASE / "simulator/worlds/arena.wbt" + + if not world_file.exists(): + raise RuntimeError("World file not found.") + + if not (SIM_BASE / "venv").exists(): + raise RuntimeError("Please run the setup.py script before running the simulator.") + + # Check setup finish successfully + if not (SIM_BASE / "venv/setup_success").exists(): + raise RuntimeError("Setup has not completed successfully. Please re-run the setup.py script.") + + # Check if Webots is in the PATH + webots = which("webots") + + # Find the webots executable, if it is not in the PATH + if webots is None: + for system_filter, path in POSSIBLE_WEBOTS_PATHS: + if sys.platform.startswith(system_filter): + print(f"Checking {path}") + if Path(path).exists(): + webots = path + break + + if webots is None or not Path(webots).exists(): + raise RuntimeError("Webots executable not found.") + + return Path(webots), world_file + + +def main() -> None: + """Run the project in Webots.""" + try: + webots, world_file = get_webots_parameters() + + # Run the world file in Webots, + # detaching the process so it does not close when this script does + if sys.platform == "win32": + Popen( + [str(webots), str(world_file)], + creationflags=DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP, + # shell=True is needed to run from shortcuts + shell=(webots.suffix == ".lnk"), + ) + else: + Popen([str(webots), str(world_file)], start_new_session=True) + except RuntimeError as e: + print(BOLD_RED) + print(f"An error occurred: \n{e}") + input(f"Press enter to continue...{RESET_COLOUR}") + exit(1) + except Exception as e: + print(BOLD_RED) + print(f"An error occurred: {e}") + print(traceback.format_exc()) + input(f"Press enter to continue...{RESET_COLOUR}") + exit(1) + + +if __name__ == "__main__": + main() diff --git a/scripts/run_comp_match.py b/scripts/run_comp_match.py new file mode 100755 index 0000000..9754fd2 --- /dev/null +++ b/scripts/run_comp_match.py @@ -0,0 +1,298 @@ +#!/usr/bin/env python3 +"""A script to run a competition match.""" +from __future__ import annotations + +import argparse +import json +import os +import shutil +import subprocess +import sys +from pathlib import Path +from tempfile import TemporaryDirectory +from zipfile import ZipFile + +if (Path(__file__).parents[1] / 'simulator/VERSION').exists(): + # Running in release mode, run_simulator will be in folder above + sys.path.append(str(Path(__file__).parents[1])) + +from run_simulator import get_webots_parameters + +NUM_ZONES = 4 +GAME_DURATION_SECONDS = 150 + + +class MatchParams(argparse.Namespace): + """Parameters for running a competition match.""" + + archives_dir: Path + match_num: int + teams: list[str] + duration: int + video_enabled: bool + video_resolution: tuple[int, int] + + +def load_team_code( + usercode_dir: Path, + arena_root: Path, + match_parameters: MatchParams, +) -> None: + """Load the team code into the arena root.""" + for zone_id, tla in enumerate(match_parameters.teams): + zone_path = arena_root / f"zone_{zone_id}" + + if zone_path.exists(): + shutil.rmtree(zone_path) + + if tla == '-': + # no team in this zone + continue + + zone_path.mkdir() + with ZipFile(usercode_dir / f'{tla}.zip') as zipfile: + zipfile.extractall(zone_path) + + +def generate_match_file(save_path: Path, match_parameters: MatchParams) -> None: + """Write the match file to the arena root.""" + match_file = save_path / 'match.json' + + # Use a format that is compatible with SRComp + match_file.write_text(json.dumps( + { + 'match_number': match_parameters.match_num, + 'arena_id': 'Simulator', + 'teams': { + tla: {'zone': idx} + for idx, tla in enumerate(match_parameters.teams) + if tla != '-' + }, + 'duration': match_parameters.duration, + 'recording_config': { + 'enabled': match_parameters.video_enabled, + 'resolution': list(match_parameters.video_resolution) + } + }, + indent=4, + )) + + +def set_comp_mode(arena_root: Path) -> None: + """Write the mode file to indicate that the competition is running.""" + (arena_root / 'mode.txt').write_text('comp') + + +def archive_zone_files( + team_archives_dir: Path, + arena_root: Path, + zone: int, + match_id: str, +) -> None: + """Zip the files in the zone directory and save them to the team archives directory.""" + zone_dir = arena_root / f'zone_{zone}' + + shutil.make_archive(str(team_archives_dir / f'{match_id}-zone-{zone}'), 'zip', zone_dir) + + +def archive_zone_folders( + archives_dir: Path, + arena_root: Path, + teams: list[str], + match_id: str, +) -> None: + """Zip the zone folders and save them to the archives directory.""" + for zone_id, tla in enumerate(teams): + if tla == '-': + # no team in this zone + continue + + tla_dir = archives_dir / tla + tla_dir.mkdir(exist_ok=True) + + archive_zone_files(tla_dir, arena_root, zone_id, match_id) + + +def archive_match_recordings(archives_dir: Path, arena_root: Path, match_id: str) -> None: + """Copy the video, animation, and image files to the archives directory.""" + recordings_dir = archives_dir / 'recordings' + recordings_dir.mkdir(exist_ok=True) + + match_recordings = arena_root / 'recordings' + + # Copy the video file + video_file = match_recordings / f'{match_id}.mp4' + if video_file.exists(): + shutil.copy(video_file, recordings_dir) + + # Copy the animation files + animation_files = [ + match_recordings / f'{match_id}.html', + match_recordings / f'{match_id}.json', + match_recordings / f'{match_id}.x3d', + match_recordings / f'{match_id}.css', + ] + for animation_file in animation_files: + shutil.copy(animation_file, recordings_dir) + + # Copy the animation textures + # Every match will have the same textures, so we only need one copy of them + textures_dir = match_recordings / 'textures' + shutil.copytree(textures_dir, recordings_dir / 'textures', dirs_exist_ok=True) + + # Copy the image file + image_file = match_recordings / f'{match_id}.jpg' + shutil.copy(image_file, recordings_dir) + + +def archive_match_file(archives_dir: Path, match_file: Path, match_number: int) -> None: + """ + Copy the match file (which may contain scoring data) to the archives directory. + + This also renames the file to be compatible with SRComp. + """ + matches_dir = archives_dir / 'matches' + matches_dir.mkdir(exist_ok=True) + + # SRComp expects YAML files. JSON is a subset of YAML, so we can just rename the file. + completed_match_file = matches_dir / f'{match_number:0>3}.yaml' + + shutil.copy(match_file, completed_match_file) + + +def archive_supervisor_log(archives_dir: Path, arena_root: Path, match_id: str) -> None: + """Archive the supervisor log file.""" + log_archive_dir = archives_dir / 'supervisor_logs' + log_archive_dir.mkdir(exist_ok=True) + + log_file = arena_root / f'supervisor-log-{match_id}.txt' + + shutil.copy(log_file, log_archive_dir) + + +def execute_match(arena_root: Path) -> None: + """Run Webots with the right world.""" + # Webots is only on the PATH on Linux so we have a helper function to find it + try: + webots, world_file = get_webots_parameters() + except RuntimeError as e: + raise FileNotFoundError(e) + + sim_env = os.environ.copy() + sim_env['ARENA_ROOT'] = str(arena_root) + try: + subprocess.check_call( + [ + str(webots), + '--batch', + '--stdout', + '--stderr', + '--mode=realtime', + str(world_file), + ], + env=sim_env, + ) + except subprocess.CalledProcessError as e: + # TODO review log output here + raise RuntimeError(f"Webots failed with return code {e.returncode}") from e + + +def run_match(match_parameters: MatchParams) -> None: + """Run the match in a temporary directory and archive the results.""" + with TemporaryDirectory(suffix=f'match-{match_parameters.match_num}') as temp_folder: + arena_root = Path(temp_folder) + match_num = match_parameters.match_num + match_id = f'match-{match_num}' + archives_dir = match_parameters.archives_dir + + # unzip teams code into zone_N folders under this folder + load_team_code(archives_dir, arena_root, match_parameters) + # Create info file to tell the comp supervisor what match this is + # and how to handle recordings + generate_match_file(arena_root, match_parameters) + # Set mode file to comp + set_comp_mode(arena_root) + + try: + # Run webots with the right world + execute_match(arena_root) + except (FileNotFoundError, RuntimeError) as e: + print(f"Failed to run match: {e}") + # Save the supervisor log as it may contain useful information + archive_supervisor_log(archives_dir, arena_root, match_id) + raise + + # Archive the supervisor log first in case any collation fails + archive_supervisor_log(archives_dir, arena_root, match_id) + # Zip up and collect all files for each zone + archive_zone_folders(archives_dir, arena_root, match_parameters.teams, match_id) + # Collect video, animation & image + archive_match_recordings(archives_dir, arena_root, match_id) + # Collect ancillary files + archive_match_file(archives_dir, arena_root / 'match.json', match_num) + + +def parse_args() -> MatchParams: + """Parse command line arguments.""" + parser = argparse.ArgumentParser(description="Run a competition match.") + + parser.add_argument( + 'archives_dir', + help=( + "The directory containing the teams' robot code, as Zip archives " + "named for the teams' TLAs. This directory will also be used as the " + "root for storing the resulting logs and recordings." + ), + type=Path, + ) + parser.add_argument( + 'match_num', + type=int, + help="The number of the match to run.", + ) + parser.add_argument( + 'teams', + nargs=NUM_ZONES, + help=( + "TLA of the team in each zone, in order from zone 0 to " + f"{NUM_ZONES - 1}. Use dash (-) for an empty zone. " + "Must specify all zones." + ), + metavar='tla', + ) + parser.add_argument( + '--duration', + help="The duration of the match (in seconds).", + type=int, + default=GAME_DURATION_SECONDS, + ) + parser.add_argument( + '--no-record', + help=( + "Inhibit creation of the MPEG video, the animation is unaffected. " + "This can greatly increase the execution speed on GPU limited systems " + "when the video is not required." + ), + action='store_false', + dest='video_enabled', + ) + parser.add_argument( + '--resolution', + help="Set the resolution of the produced video.", + type=int, + nargs=2, + default=[1920, 1080], + metavar=('width', 'height'), + dest='video_resolution', + ) + return parser.parse_args(namespace=MatchParams()) + + +def main() -> None: + """Run a competition match entrypoint.""" + match_parameters = parse_args() + run_match(match_parameters) + + +if __name__ == '__main__': + main() diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..db75d0a --- /dev/null +++ b/setup.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +""" +A script to setup the environment for running the project in Webots. + +It will: +1. Create a virtual environment in the project root, if it does not exist +2. Install the dependencies from requirements.txt in the virtual environment +3. Set the python path in runtime.ini to the virtual environment python +4. Repopulate the zone 0 folder with basic_robot.py if robot.py is missing +""" +from __future__ import annotations + +import logging +import platform +import shutil +import sys +from pathlib import Path +from subprocess import SubprocessError, check_call +from venv import create + +logging.basicConfig(level=logging.INFO, format="[%(asctime)s] %(levelname)s: %(message)s") +logger = logging.getLogger(__name__) + +BOLD_RED = '\x1b[31;1m' +GREEN = '\x1b[32;20m' +RESET_COLOUR = '\x1b[0m' + + +def populate_python_config(runtime_ini: Path, venv_python: Path) -> None: + """ + Populate the python configuration in the runtime.ini file. + + This will set the python command to the virtual environment python. + + :param runtime_ini: The path to the runtime.ini file + :param venv_python: The path to the virtual environment python executable + """ + runtime_content: list[str] = [] + if runtime_ini.exists(): + prev_runtime_content = runtime_ini.read_text().splitlines() + else: + prev_runtime_content = [] + + # Remove previous python settings from runtime.ini + # Everything between [python] and the next section is removed + in_python_section = False + for line in prev_runtime_content: + if line == "[python]": + in_python_section = True + if runtime_content and runtime_content[-1] == "": + runtime_content.pop() + elif in_python_section and line.startswith("["): + in_python_section = False + elif not in_python_section: + runtime_content.append(line) + + runtime_content.extend([ + "", + "[python]", + f"COMMAND = {venv_python.absolute()}", + "", + ]) + + runtime_ini.write_text('\n'.join(runtime_content)) + + +try: + if (Path(__file__).parent / 'simulator/VERSION').exists(): + # This is running from a release + print("Running in release mode") + project_root = Path(__file__).parent + requirements = project_root / "simulator/requirements.txt" + else: + # This is running from the repository + print("Running in development mode") + project_root = Path(__file__).parents[1] + requirements = project_root / "requirements.txt" + + print(f"Python version: {sys.version} on {platform.platform()}") + + venv_dir = project_root / "venv" + + # Reset success flag + (venv_dir / "setup_success").unlink(missing_ok=True) + + logger.info(f"Creating virtual environment in {venv_dir.absolute()}") + create(venv_dir, with_pip=True) + + logger.info(f"Installing dependencies from {requirements.absolute()}") + if platform.system() == "Windows": + pip = venv_dir / "Scripts/pip.exe" + venv_python = venv_dir / "Scripts/python" + else: + pip = venv_dir / "bin/pip" + venv_python = venv_dir / "bin/python" + check_call( + [str(venv_python), "-m", "pip", "install", "--upgrade", "pip", "setuptools", "wheel"], + cwd=venv_dir, + ) + check_call( + [str(pip), "install", "--only-binary=:all:", "-r", str(requirements)], + cwd=venv_dir, + ) + + logger.info("Preloading OpenCV & sr.robot3") + check_call([str(venv_python), "-c", "import cv2;import sr.robot3"], cwd=venv_dir) + + logger.info("Setting up Webots Python location") + + controllers_dir = project_root / "simulator/controllers" + usercode_ini = controllers_dir / "usercode_runner/runtime.ini" + supervisor_ini = controllers_dir / "competition_supervisor/runtime.ini" + populate_python_config(usercode_ini, venv_python) + populate_python_config(supervisor_ini, venv_python) + + # Mark that we succeeded + (venv_dir / "setup_success").touch() + + # repopulate zone 0 with example code if robot.py is missing + zone_0 = project_root / "zone_0" + if not (zone_0 / "robot.py").exists(): + logger.info("Repopulating zone 0 with example code") + zone_0.mkdir(exist_ok=True) + shutil.copy(project_root / "example_robots/basic_robot.py", zone_0 / "robot.py") +except SubprocessError: + print(BOLD_RED) + logger.error("Setup failed due to an error.") + input(f"An error occurred, press enter to close.{RESET_COLOUR}") +except Exception: + print(BOLD_RED) + logger.exception("Setup failed due to an error.") + input(f"An error occurred, press enter to close.{RESET_COLOUR}") +else: + input(f"{GREEN}Setup complete, press enter to close.{RESET_COLOUR}") diff --git a/simulator/VERSION b/simulator/VERSION new file mode 100644 index 0000000..e7f7668 --- /dev/null +++ b/simulator/VERSION @@ -0,0 +1 @@ +2026.0.1 \ No newline at end of file diff --git a/simulator/__pycache__/environment.cpython-310.pyc b/simulator/__pycache__/environment.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8bc9ca6a3de26386362aa34f8a6e8bdf324cd0d1 GIT binary patch literal 1151 zcmZuwPjAyO6t~l~$+EQUpSYkQb3zrUy(2VHZ5^AmwrK4*me5{3wF+ z0zUZ(3`GH=m|6)Aa6Vgs1#=-OjO@V1h}o<-!B?dz8ajc?Txvf+!2(3_(7RfsMdl8j zsWriY&-|dwd|HD23Uz{e)D4!Hf3-YC@)=v93(WswT>|nwre(TF{SS7q%2p=`BWiU~ zwQ~N^+vMp{e4GhJ)PNCrCTrtJ4M-A8MUJ>2@o3D2iqm5v<58AG3SuKpvxLc-=QR?^ zNuQ@Gic@J8Qbnnf`N`7(gIgs#VIoRy?}1Hn6vJ5(!Cu1qkz%ywT>~qqUB%jd15nbv{4oaPyoK%s&61~G7!rF|9i)X;b%iM&G- z#YvPT{B%wjew5?lw4Y>@(fr0c#4?G5X}Dco&;?Lh+lC!o0_m7g5K=<>tP$q6$h?@%0n89uShHb_}5N-z(=euvNUC)e#*t0x;*46 z{9Dg|?u?5198tCLnO>G4*EpPuu0sN8-r0&%!5&`1_HVHQZq*VFOzrBLcEXVIK9p*4 zDO?C@$Ml%Qhj}leY(nnr?+_Jm1Gl@|x@p(X58P(dg=3}!JZ5-%L8sl!AwlJ3qty(7 zq-MCiyVukU?Socmw!6Xot>$*)V6PXp8okY(aO+^d(cA5`^&&7d%c#BAQIfHy5M1Q7 q=d8_{<-EY>HxFRLs3jkQv5{MJv5za3i`Oh0yVy3iP$_x^ulxrI(MS6L literal 0 HcmV?d00001 diff --git a/simulator/__pycache__/environment.cpython-313.pyc b/simulator/__pycache__/environment.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a15913eee8f593acb56c83c0ce2c622bfa6589fb GIT binary patch literal 1698 zcmah}&rcgi6rQyg?|S{Km;~Acl#LT9I21cgN))w-)D2Fd0XyBrJrql;#UAiR>s@Ph zOahlUmqt-l1you%w-m3r_+atytt54L#lKJ+~ZMS zu`dC`e!kVbXjGDN!Bi9c1Y_pD`%r5h;ykuj!#BzAT*yE?)q2#h>-m?RxgUgYDDoRiHs;#Tsyx>{qMNjj( z?x8D#E-%WSw#-ws+T!`2$dC?KEl>deUEhZ^m@b3=@ltx*IlIRK@G4+@pp5XZel4jG z%k`Q|2WTxD$hNAG!A601DdN$9!ar9rDAskWgbmwb@VBYtoWLb)%eraW_qzyXFS>q) zRWj>kSe8y87-Debl0o99%p^QRlLRQ$Ai~sy5ed)cCkr!K)#V6ckP=bT7{xS=1ZtFJ z7A64%tM)Ra{1P#(SSuUIVcnI<&2+`C!nCsj%V}J#rJV&Ej~>-AdiBcH>!~ZL_tHnE zbXQAKwH4CqKwPhBT~^dbBM;Hfxl4xzcoPLZ;^V1DQ;+kH@@tt5=h=8`VE8va`6?FQ zc>kIB&BRXMaIDhM6*h@%+PW{Bc z1TkWCC&4fBxvc9#qMu}Dvl?x5R=YhtlO@4iVOC??D(Roh-p&+e=Cs+&+^s2XvY=$< zrt>)xqFwef66tf@tivoqHgcQu0wDH!qPSbhc>8CcKo{wfq2hc`hkZZCaR)x0;}0dk zNxuW>5AgQuP;5>7YGNmpXoeE4(8Zn5NHa9j5O-q(TghhZLL>Y-(z}+}yu5ySGqs+2 zKKR3fpB}z=*oxn5MLudsyW##uaJTpD*1cx$a3lI>I6{~23~qz|{QwZ-JHk*?7}^oe qHHC8r5(u5%9{Bi|4}MK;pZ=^RDcijACLnQ%gEN4OzXW~m75)J<_=!yb literal 0 HcmV?d00001 diff --git a/simulator/controllers/competition_supervisor/competition_supervisor.py b/simulator/controllers/competition_supervisor/competition_supervisor.py new file mode 100644 index 0000000..1e73f0b --- /dev/null +++ b/simulator/controllers/competition_supervisor/competition_supervisor.py @@ -0,0 +1,263 @@ +""".""" +from __future__ import annotations + +import sys +import time +from contextlib import contextmanager +from pathlib import Path +from typing import Iterator + +from controller import Supervisor + +# Robot constructor lacks a return type annotation in R2023b +sys.path.insert(0, Supervisor().getProjectPath()) # type: ignore[no-untyped-call] +# from lighting_control import LightingControl +import environment # configure path to include modules +from robot_logging import get_match_identifier, prefix_and_tee_streams +from robot_utils import get_game_mode, get_match_data, get_robot_file + +# Get the robot object that was created when setting up the environment +_robot = Supervisor.created +assert _robot is not None, "Robot object not created" +supervisor: Supervisor = _robot # type: ignore[assignment] + + +class RobotData: + """Data about a robot in the arena.""" + + def __init__(self, zone: int): + self.registered_ready = False + self.zone = zone + self.robot = supervisor.getFromDef(f'ROBOT{zone}') + if self.robot is None: + raise ValueError(f"Failed to get Webots node for zone {zone}") + + def zone_occupied(self) -> bool: + """Check if this zone has a robot.py file associated with it.""" + try: + _ = get_robot_file(self.zone) + except FileNotFoundError: + return False + return True + + def remove_robot(self) -> None: + """Delete the robot proto from the world.""" + self.robot.remove() # type: ignore[attr-defined] + + def preset_robot(self) -> None: + """Arm the robot so that it waits for the start signal.""" + self.robot.getField('customData').setSFString('prestart') # type: ignore[attr-defined] + + def robot_ready(self) -> bool: + """Check if robot has set its pre-start flag.""" + return bool(self.robot.getField('customData').getSFString() == 'ready') # type: ignore[attr-defined] + + def start_robot(self) -> None: + """Signal to the robot that the start button has been pressed.""" + self.robot.getField('customData').setSFString('start') # type: ignore[attr-defined] + + +class Robots: + """A collection of robots in the arena.""" + + def __init__(self) -> None: + self.robots: dict[int, RobotData] = {} + + for zone in range(0, environment.NUM_ZONES): + try: + robot_data = RobotData(zone) + except ValueError as e: + print(e) + else: + self.robots[zone] = robot_data + + def remove_unoccupied_robots(self) -> None: + """Remove all robots that don't have usercode.""" + for robot in list(self.robots.values()): + if not robot.zone_occupied(): + robot.remove_robot() + _ = self.robots.pop(robot.zone) + + def preset_robots(self) -> None: + """Arm all robots so that they wait for the start signal.""" + for robot in self.robots.values(): + robot.preset_robot() + + def wait_for_ready(self, timeout: float) -> None: + """Wait for all robots to set their pre-start flags.""" + end_time = supervisor.getTime() + timeout + while supervisor.getTime() < end_time: + all_ready = True + # Sleep in individual timesteps to allow the robots to update + supervisor.step() + + for zone, robot in self.robots.items(): + if not robot.registered_ready: + if robot.robot_ready(): + print(f"Robot in zone {zone} is ready.") + # Log only once per robot when ready + robot.registered_ready = True + else: + all_ready = False + if all_ready: + break + else: + pending_robots = ', '.join([ + str(zone) + for zone, robot in self.robots.items() + if not robot.robot_ready() + ]) + raise TimeoutError( + f"Robots in zones {pending_robots} failed to initialise. " + f"Failed to reach wait_start() within {timeout} seconds." + ) + + def start_robots(self) -> None: + """Signal to all robots that their start buttons have been pressed.""" + for robot in self.robots.values(): + robot.start_robot() + + +def is_dev_mode() -> bool: + """Load the mode file and check if we are in dev mode.""" + return (get_game_mode() == 'dev') + + +@contextmanager +def record_animation(filename: Path) -> Iterator[None]: + """Record an animation for the duration of the manager.""" + filename.parent.mkdir(parents=True, exist_ok=True) + print(f"Saving animation to {filename}") + supervisor.animationStartRecording(str(filename)) + yield + supervisor.animationStopRecording() # type: ignore[no-untyped-call] + + +@contextmanager +def record_video( + filename: Path, + resolution: tuple[int, int], + skip: bool = False +) -> Iterator[None]: + """Record a video for the duration of the manager.""" + filename.parent.mkdir(parents=True, exist_ok=True) + + if skip: + print('Not recording movie') + yield + return + else: + print(f"Saving video to {filename}") + + supervisor.movieStartRecording( + str(filename), + width=resolution[0], + height=resolution[1], + quality=100, + codec=0, + acceleration=1, + caption=False, + ) + yield + supervisor.movieStopRecording() # type: ignore[no-untyped-call] + + while not supervisor.movieIsReady(): # type: ignore[no-untyped-call] + time.sleep(0.1) + + if supervisor.movieFailed(): # type: ignore[no-untyped-call] + print("Movie failed to record") + + +def save_image(filename: Path) -> None: + """Capture an image of the arena.""" + filename.parent.mkdir(parents=True, exist_ok=True) + print(f"Saving image to {filename}") + supervisor.exportImage(str(filename), 100) + + +def run_match( + match_duration: int, + media_path_stem: Path, + video_resolution: tuple[int, int], + skip_video: bool, +) -> None: + """Run a match in the arena.""" + robots = Robots() + robots.remove_unoccupied_robots() + + time_step = int(supervisor.getBasicTimeStep()) + match_timesteps = (match_duration * 1000) // time_step + # lighting_control = LightingControl(supervisor, match_timesteps) + + robots.preset_robots() + + robots.wait_for_ready(5) + + with record_animation(media_path_stem.with_suffix('.html')): + # Animations don't support lighting changes so start the animation before + # setting the lighting. Step the simulation to allow the animation to start. + supervisor.step() + # Set initial lighting + # lighting_control.service_lighting(0) + with record_video(media_path_stem.with_suffix('.mp4'), video_resolution, skip_video): + print("===========") + print("Match start") + print("===========") + + # We are ready to start the match now. "Press" the start button on the robots + robots.start_robots() + supervisor.simulationSetMode(Supervisor.SIMULATION_MODE_FAST) # type: ignore[attr-defined] + + # for current_step in range(match_timesteps + 1): + # lighting_control.service_lighting(current_step) + # supervisor.step(time_step) + supervisor.step(match_timesteps) + + print("==================") + print("Game over, pausing") + print("==================") + supervisor.simulationSetMode(Supervisor.SIMULATION_MODE_PAUSE) # type: ignore[attr-defined] + + # To allow for a clear image of the final state, we have reset the + # lighting after the final frame of the video. + save_image(media_path_stem.with_suffix('.jpg')) + # TODO score match + + +def main() -> None: + """Run the competition supervisor.""" + if is_dev_mode(): + robots = Robots() + robots.remove_unoccupied_robots() + exit() + + match_data = get_match_data() + match_id = get_match_identifier() + + prefix_and_tee_streams( + environment.ARENA_ROOT / f'supervisor-log-{match_id}.txt', + prefix=lambda: f'[{supervisor.getTime():0.3f}] ', + ) + + try: + # TODO check for required libraries? + + run_match( + match_data.match_duration, + environment.ARENA_ROOT / 'recordings' / match_id, + video_resolution=match_data.video_resolution, + skip_video=(not match_data.video_enabled), + ) + # Set the overall Webots exit code to follow the supervisor's exit code + except Exception as e: + # Print and step so error is printed to console + print(f"Error: {e}") + supervisor.step() + supervisor.simulationQuit(1) + raise + else: + supervisor.simulationQuit(0) + + +if __name__ == '__main__': + main() diff --git a/simulator/controllers/competition_supervisor/lighting_control.py b/simulator/controllers/competition_supervisor/lighting_control.py new file mode 100644 index 0000000..30a508c --- /dev/null +++ b/simulator/controllers/competition_supervisor/lighting_control.py @@ -0,0 +1,287 @@ +""" +The controller for altering arena lighting provided by a DirectionalLight and a Background. + +Currently doesn't support: +- Timed pre-match lighting changes +""" +from __future__ import annotations + +from typing import NamedTuple + +from controller import Node, Supervisor + +MATCH_LIGHTING_INTENSITY = 1.5 +DEFAULT_LUMINOSITY = 1 + + +class FromEnd(NamedTuple): + """ + Represents a time relative to the end of the match. + + Negative values are times before the end of the match. 0 is the last frame + of the video. All positive values will only appear in the post-match image. + """ + + time: float + + +class ArenaLighting(NamedTuple): + """Represents a lighting configuration for the arena.""" + + light_def: str + intensity: float + colour: tuple[float, float, float] = (1, 1, 1) + + +class LightingEffect(NamedTuple): + """Represents a lighting effect to be applied to the arena.""" + + start_time: float | FromEnd + fade_time: float | None = None + lighting: ArenaLighting = ArenaLighting('SUN', intensity=MATCH_LIGHTING_INTENSITY) + luminosity: float = DEFAULT_LUMINOSITY + name: str = "" + + def __repr__(self) -> str: + light = self.lighting + lights_info = [ + f"({light.light_def}, int={light.intensity}, col={light.colour})" + ] + return ( + f"" + ) + + +class LightingStep(NamedTuple): + """Represents a step in a lighting fade.""" + + timestep: int + light_node: Node + intensity: float | None + colour: tuple[float, float, float] | None + luminosity: float | None + name: str | None = None + + +CUE_STACK = [ + LightingEffect( + 0, + lighting=ArenaLighting('SUN', intensity=0.2), + luminosity=0.05, + name="Pre-set", + ), + LightingEffect( + 0, + fade_time=1.5, + lighting=ArenaLighting('SUN', intensity=MATCH_LIGHTING_INTENSITY), + luminosity=1, + name="Fade-up", + ), + LightingEffect( + FromEnd(0), # This time runs this cue as the last frame of the video + lighting=ArenaLighting('SUN', intensity=1, colour=(0.8, 0.1, 0.1)), + luminosity=0.1, + name="End of match", + ), + LightingEffect( + FromEnd(1), + lighting=ArenaLighting('SUN', intensity=MATCH_LIGHTING_INTENSITY), + luminosity=DEFAULT_LUMINOSITY, + name="Post-match image", + ), +] + + +class LightingControl: + """Controller for managing lighting effects in the arena.""" + + def __init__(self, supervisor: Supervisor, duration: int) -> None: + self._robot = supervisor + self._final_timestep = duration + self.timestep = self._robot.getBasicTimeStep() + self.ambient_node = supervisor.getFromDef('AMBIENT') + + # fetch all nodes used in effects, any missing nodes will be flagged here + light_names = set(effect.lighting.light_def for effect in CUE_STACK) + self.lights = { + name: supervisor.getFromDef(name) + for name in light_names + } + missing_lights = [name for name, light in self.lights.items() if light is None] + if missing_lights: + raise ValueError(f"Missing light nodes: {missing_lights}") + + # Convert FromEnd times to absolute times + cue_stack = self.convert_from_end_times(CUE_STACK) + + self.lighting_steps = self.generate_lighting_steps(cue_stack) + + def convert_from_end_times(self, cue_stack: list[LightingEffect]) -> list[LightingEffect]: + """Convert FromEnd times to absolute times.""" + new_cue_stack = [] + end_time = (self._final_timestep * self.timestep) / 1000 + # @ 25 fps the last 5 timesteps are not included in the video + start_of_frame_offset = self.timestep * 6 / 1000 + for cue in cue_stack: + if isinstance(cue.start_time, FromEnd): + abs_time = end_time + cue.start_time.time - start_of_frame_offset + new_cue_stack.append(cue._replace(start_time=abs_time)) + else: + new_cue_stack.append(cue) + + return new_cue_stack + + def generate_lighting_steps(self, cue_stack: list[LightingEffect]) -> list[LightingStep]: + """Expand the cue stack into a list of lighting steps.""" + steps: list[LightingStep] = [] + + # Generate current values for all lights + current_values = { + name: LightingStep( + 0, + light, + light.getField('intensity').getSFFloat(), # type: ignore[attr-defined] + light.getField('color').getSFColor(), # type: ignore[attr-defined] + 0, + ) + for name, light in self.lights.items() + } + current_luminosity = self.ambient_node.getField('luminosity').getSFFloat() # type: ignore[attr-defined] + + for cue in cue_stack: + # Get the current state of the light with the current luminosity + current_state = current_values[cue.lighting.light_def] + current_state = current_state._replace(luminosity=current_luminosity) + + expanded_cue = self.expand_lighting_fade(cue, current_state) + + # Update current values from the last step of the cue + current_values[cue.lighting.light_def] = expanded_cue[-1] + current_luminosity = expanded_cue[-1].luminosity + + steps.extend(expanded_cue) + + steps.sort(key=lambda x: x.timestep) + # TODO optimise steps to remove duplicate steps + + return steps + + def expand_lighting_fade( + self, + cue: LightingEffect, + current_state: LightingStep, + ) -> list[LightingStep]: + """Expand a fade effect into a list of steps.""" + fades = [] + + assert isinstance(cue.start_time, (float, int)), \ + "FromEnd times should be converted to absolute times" + cue_start = int((cue.start_time * 1000) / self.timestep) + + if cue.fade_time is None: + # no fade, just set values + return [LightingStep( + cue_start, + self.lights[cue.lighting.light_def], + cue.lighting.intensity, + cue.lighting.colour, + cue.luminosity, + cue.name + )] + + assert current_state.intensity is not None, "Current intensity should be set" + assert current_state.colour is not None, "Current colour should be set" + assert current_state.luminosity is not None, "Current luminosity should be set" + + fade_steps = int((cue.fade_time * 1000) / self.timestep) + if fade_steps == 0: + fade_steps = 1 + intensity_step = (cue.lighting.intensity - current_state.intensity) / fade_steps + colour_step = [ + (cue.lighting.colour[0] - current_state.colour[0]) / fade_steps, + (cue.lighting.colour[1] - current_state.colour[1]) / fade_steps, + (cue.lighting.colour[2] - current_state.colour[2]) / fade_steps, + ] + luminosity_step = (cue.luminosity - current_state.luminosity) / fade_steps + + for step in range(fade_steps): + fades.append( + LightingStep( + cue_start + step, + self.lights[cue.lighting.light_def], + current_state.intensity + intensity_step * step, + ( + current_state.colour[0] + (colour_step[0] * step), + current_state.colour[1] + (colour_step[1] * step), + current_state.colour[2] + (colour_step[2] * step), + ), + current_state.luminosity + luminosity_step * step, + cue.name if step == 0 else None, + ) + ) + + # Replace the last step with the final values + fades.pop() + fades.append(LightingStep( + cue_start + fade_steps, + self.lights[cue.lighting.light_def], + cue.lighting.intensity, + cue.lighting.colour, + cue.luminosity, + )) + + return fades + + def set_luminosity(self, luminosity: float) -> None: + """Set the luminosity of the ambient node.""" + self.ambient_node.getField('luminosity').setSFFloat(float(luminosity)) # type: ignore[attr-defined] + + def set_node_intensity(self, node: Node, intensity: float) -> None: + """Set the intensity of a node.""" + node.getField('intensity').setSFFloat(float(intensity)) # type: ignore[attr-defined] + + def set_node_colour(self, node: Node, colour: tuple[float, float, float]) -> None: + """Set the colour of a node.""" + node.getField('color').setSFColor(list(colour)) # type: ignore[attr-defined] + + def service_lighting(self, current_timestep: int) -> int: + """Service the lighting effects for the current timestep.""" + index = 0 + + if current_timestep >= self._final_timestep and self.lighting_steps: + # Run all remaining steps + current_timestep = self.lighting_steps[-1].timestep + + while ( + len(self.lighting_steps) > index + and self.lighting_steps[index].timestep == current_timestep + ): + lighting_step = self.lighting_steps[index] + if lighting_step.name is not None: + print( + f"Running lighting effect: {lighting_step.name} @ " + f"{current_timestep * self.timestep / 1000}" + ) + + if lighting_step.intensity is not None: + self.set_node_intensity(lighting_step.light_node, lighting_step.intensity) + + if lighting_step.colour is not None: + self.set_node_colour(lighting_step.light_node, lighting_step.colour) + + if lighting_step.luminosity is not None: + self.set_luminosity(lighting_step.luminosity) + + index += 1 + + # Remove all steps that have been processed + self.lighting_steps = self.lighting_steps[index:] + + if self.lighting_steps: + return self.lighting_steps[0].timestep - current_timestep + else: + return -1 diff --git a/simulator/controllers/competition_supervisor/runtime.ini b/simulator/controllers/competition_supervisor/runtime.ini new file mode 100644 index 0000000..a51cc52 --- /dev/null +++ b/simulator/controllers/competition_supervisor/runtime.ini @@ -0,0 +1,3 @@ + +[python] +COMMAND = /home/syed/tmp/sbot-simulator-2026.0.1/venv/bin/python diff --git a/simulator/controllers/usercode_runner/runtime.ini b/simulator/controllers/usercode_runner/runtime.ini new file mode 100644 index 0000000..a51cc52 --- /dev/null +++ b/simulator/controllers/usercode_runner/runtime.ini @@ -0,0 +1,3 @@ + +[python] +COMMAND = /home/syed/tmp/sbot-simulator-2026.0.1/venv/bin/python diff --git a/simulator/controllers/usercode_runner/usercode_runner.py b/simulator/controllers/usercode_runner/usercode_runner.py new file mode 100644 index 0000000..ce87da3 --- /dev/null +++ b/simulator/controllers/usercode_runner/usercode_runner.py @@ -0,0 +1,150 @@ +""" +The entry point for all robot controllers. + +This script is responsible for setting up the environment, starting the devices, +and running the usercode. + +The board simulators are run in a separate thread to allow the usercode to run +in the main thread. This provides the interface between the sr-robot3 module and Webots. +""" +import atexit +import json +import logging +import os +import runpy +import sys +import threading +from pathlib import Path +from tempfile import TemporaryDirectory + +from controller import Robot + +# Robot constructor lacks a return type annotation in R2023b +sys.path.insert(0, Robot().getProjectPath()) # type: ignore[no-untyped-call] +import environment # configure path to include modules +from robot_logging import get_match_identifier, prefix_and_tee_streams +from robot_utils import get_game_mode, get_robot_file, print_simulation_version +from sbot_interface.setup import setup_devices +from sbot_interface.socket_server import SocketServer + +# Get the robot object that was created when setting up the environment +_robot = Robot.created +assert _robot is not None, "Robot object not created" +robot = _robot + +LOGGER = logging.getLogger('usercode_runner') + + +def start_devices() -> SocketServer: + """ + Create the board simulators and return the SocketServer object. + + Using the links or links_formatted method of the SocketServer object, the + devices' socket addresses can be accessed and passed to the usercode. + + The WEBOTS_DEVICE_LOGGING environment variable, overrides the log level used. + Default is WARNING. + """ + if log_level := os.environ.get('WEBOTS_DEVICE_LOGGING'): + return setup_devices(log_level) + else: + return setup_devices() + + +def run_usercode(robot_file: Path, robot_zone: int, game_mode: str) -> None: + """ + Run the user's code from the given file. + + Metadata is created in a temporary directory and passed to the usercode. + The system path is modified to avoid the controller modules being imported + in the usercode. + + :param robot_file: The path to the robot file + :param robot_zone: The zone number + :param game_mode: The game mode string ('dev' or 'comp') + :raises Exception: If the usercode raises an exception + """ + # Remove this folder from the path + sys.path.remove(str(Path.cwd())) + # Remove our custom modules from the path + sys.path.remove(str(environment.MODULES_ROOT)) + # Add the usercode folder to the path + sys.path.insert(0, str(robot_file.parent)) + + # Change the current directory to the usercode folder + os.chdir(robot_file.parent) + + with TemporaryDirectory() as tmpdir: + # Setup metadata (zone, game_mode) + Path(tmpdir).joinpath('astoria.json').write_text(json.dumps({ + "arena": "simulator", + "zone": robot_zone, + "mode": 'COMP' if game_mode == 'comp' else 'DEV', + })) + os.environ['SBOT_METADATA_PATH'] = tmpdir + os.environ['SBOT_USBKEY_PATH'] = str(Path.cwd()) + + # Run the usercode + # pass robot object to the usercode for keyboard robot control + runpy.run_path(str(robot_file), init_globals={'__robot__': robot}) + + +def main() -> bool: + """ + The main entry point for the usercode runner. + + This function is responsible for setting up the environment, starting the + devices, and running the usercode. + + The zone number is passed as the first argument to the script using + controllerArgs on the robot. + + On completion, the devices are stopped and atexit functions are run. + """ + zone = int(sys.argv[1]) + game_mode = get_game_mode() + + # Get the robot file + try: + robot_file = get_robot_file(zone) + except FileNotFoundError as e: + print(e.args[0]) + robot.step() + # Not having a robot file is not an error in dev mode + return game_mode != 'comp' + + # Setup log file + prefix_and_tee_streams( + robot_file.parent / f'log-zone-{zone}-{get_match_identifier()}.txt', + prefix=lambda: f'[{zone}| {robot.getTime():0.3f}] ', + ) + + # Setup devices + devices = start_devices() + + # Print the simulation version + print_simulation_version() + + # Pass the devices to the usercode + os.environ['WEBOTS_SIMULATOR'] = '1' + os.environ['WEBOTS_ROBOT'] = devices.links_formatted() + + # Start devices in a separate thread + thread = threading.Thread(target=devices.run) + thread.start() + + # Run the usercode + try: + run_usercode(robot_file, zone, game_mode) + finally: + # Run cleanup code registered in the usercode + atexit._run_exitfuncs() # noqa: SLF001 + # Cleanup devices + devices.completed = True + devices.stop_event.set() + + return True + + +if __name__ == '__main__': + exit(0 if main() else 1) diff --git a/simulator/environment.py b/simulator/environment.py new file mode 100644 index 0000000..dd82c4c --- /dev/null +++ b/simulator/environment.py @@ -0,0 +1,42 @@ +""" +Configure the sys.path list for importing simulator modules. + +Also contains constants for where several important files are located. +""" +import os +import sys +from pathlib import Path + +SIM_ROOT = Path(__file__).absolute().parent +MODULES_ROOT = SIM_ROOT / 'modules' + +ARENA_ROOT = Path(os.environ.get('ARENA_ROOT', SIM_ROOT.parent)) +ZONE_ROOT = ARENA_ROOT +GAME_MODE_FILE = ARENA_ROOT / 'mode.txt' + +NUM_ZONES = 2 +DEFAULT_MATCH_DURATION = 150 # seconds + + +if not ARENA_ROOT.is_absolute(): + # Webots sets the current directory of each controller to the directory of + # the controller file. As such, relative paths would be ambiguous. + # Hint: `$PWD` or `%CD%` may be useful to construct an absolute path from + # your relative path. + raise ValueError(f"'ARENA_ROOT' must be an absolute path, got {ARENA_ROOT!r}") + + +def setup_environment() -> None: + """ + Set up the environment for the simulator. + + This function configures the sys.path list to allow importing of the included + simulator modules. + """ + sys.path.insert(0, str(MODULES_ROOT)) + this_dir = str(Path(__file__).parent) + if this_dir in sys.path: + sys.path.remove(this_dir) + + +setup_environment() diff --git a/simulator/modules/__pycache__/robot_logging.cpython-313.pyc b/simulator/modules/__pycache__/robot_logging.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e948c5109208d053cef45a2aace941944d957f30 GIT binary patch literal 5248 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 literal 0 HcmV?d00001 diff --git a/simulator/modules/__pycache__/robot_utils.cpython-313.pyc b/simulator/modules/__pycache__/robot_utils.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0720fed2cadab5e8b6902c0192e1ba33e0d2dfc0 GIT binary patch literal 5242 zcmbVQO>7&-6`m!RTyjZK5=Bw6EK?)djzmX)wq)5>oWP1L*_QQVbEOoCR4i8HN@7gz zs$3aG7S@rv!)Fv2KWauDNZ?wK;G5^qy6thKUZ@fL zE$DTYgoTj6TeSwERtQ+Nh)^d4ty<%fJ6@kZeN|RuB4uz+PiNA4TGp_>Ea_MxGR|r8 zVlIQFBvCaDlbiyQTJJY>o;YI$Bt=nmNl&YaHU(|=rlc=J$xlgHIW?18$;jW(RVQt` z?7VCwv$F{8_t88e3>=J0V2Jx95?pW$Za4yV-Ya-IL1Lau)a@N^*NI>7f&@n;1RvoA zKeTwO6(9j22(22c6(T`cWUc9+l=S5C6-k%AX@D`33+&=)6H5zfPKOoCIGH0vR&<=D zqP;vnydn`PYd1wDmtBy_FrGOmi0z*J(y1J=w#AlrT3B7QCbA~LX$5O?QdLr#Gj=td zl2uVwq=k%}8pc0amccB7tx6C7jl9S!Vx0B5P^}4pCL?X{&8#;scuasXU*C z1;2@>R7Ey}mR;qQnr`SZL-r?U0W&7MpsE?OW}l^TS9+a8D1DwqC||47W1oY;-LOLa zr)HfkTQ%mlC|+awL{R~dh@#1hVpdJ%GPE8P#h>M*Ou4646c^J((=%yBR#fQbL@}i% zMUl`eZ$kGT{6Tj>R#wwQRkCo1X7tMF*vRa}j5s+mbNQNhWi~M~bA5V>1YsxvqCljU zDrpo&jUqTHO1e(c3priZU;+xc9rz*hYTvS&mHV_cIn}3USNgOCRqxT#*<41_Rnl{& z|IAQtfA8tO%BMbiHCi80K~$udO6y%&Bb1D+jN(r_57}Mxt&8*cw!;XYSfAK9Rb-EE zwwx@rTr^rPu1{@TF0vOlTVm^z8-qnQw%IjUU?Usq-`GLY;;2fNIR)AX!R{ltT+Gf} zP;)mb%7nzBd7m{3&N3gHs;WrIba6n9)=-IDDi(Z5*<7v|ES5$4d3j8THRSJo~8SQLqqy z(Wrm%Q{Jc_UmxH0qF}TXXf*<@r9jLG#I{0hg^o*~wtN~a;K^cWs^FV?5@;;2jTW|& zpt6zxQZ}qUgUYjD^>JI~Fq5`jq7;<2k@K?V%HK$KO)8I!GGz*Y^0XkE_ z6HTZB(Xu!|6j=}@6^JeY6=AryfJ&lfZCF-6!FB^QugHs1E(4HOac4?i?Zj!D%;{<1 zlnQ7AUx=+^ra<0{(u#WCbi>fNi<|^oOg5=zSMm+ll~pN|PT`8wFphg{0EMoOOpc0^ z(^p2tvFj6~q>T>q6In`$y8MQ2dNkmy74W!zI+*gPtWioRW2S~;rqV;6hn_+BY4ea- zknuH?xORhUFL7N4*Hz+98QiI@NLwk=YeafWk@H66d@(Xyid-@xmx_^>-|;_*9J%Lz z5{NvgEe1LZ?0Dh%mp)nhoM2Nx z^8bYlGyQ4|%>hvCl%W3MK!2 z?R$4M^7K3$lEB+QjLRxa8=8835xwYcLyJs2kpCxtlhC+?24$-da6&}mL@*UM3!M<7 z0&UZv6rv)ybF-B4CH%^0LI6*1dl?9_s1~Ba-7H66dsL`6CF?TDLV&S%vR-}+oDFP#+PVj3mOcqy%epq7Nt0Rl)ho0XIlm@>1+au8pk%aK!#7X-fpks6SS z$(4dr@uUJBvSG{vdZh z_uidt4%I|8! z9{YWnqOaRZJhS&34QTSb^`-Z9O;zmol+StPnRS)7rU+EEm|ilu@`KZQG(mVv&-cJn zDju|{g&#?bPL0^)LIGh1Ei9eK^kl$9&7vFNJ6S(e4{O$zU!AZ3n-9V%JNOc-9j1>+ zcSO2I(_V>H*XSb%L)sHGSI3ZP=#}876(CzjUq{(~y>f3>cAN{*KOlfjYiIfyb!! zF{*!z_%BfN7w8ylDZ}3Nz3sbO`*!UHbARBz{@~hsZ None: + self.streams = streams + + def write(self, data: str, /) -> int: + """ + Writes the given data to all streams in the logger. + + :param data: The data to be written to the stream. + """ + written = 0 + for stream in self.streams: + written = stream.write(data) + self.flush() + return written + + def flush(self) -> None: + """Flushes all the streams in the logger.""" + for stream in self.streams: + stream.flush() + + +class InsertPrefix(TextIOWrapper): + """Inserts a prefix into the data written to the stream.""" + + def __init__(self, stream: TextIO, prefix: Callable[[], str] | str | None) -> None: + self.stream = stream + self.prefix = prefix + self._line_start = True + + def _get_prefix(self) -> str: + if not self.prefix: + return '' + + prefix = self.prefix() if callable(self.prefix) else self.prefix + return prefix + + def write(self, data: str, /) -> int: + """ + Writes the given data to the stream, applying a prefix to each line if necessary. + + :param data: The data to be written to the stream. + """ + prefix = self._get_prefix() + if not prefix: + return self.stream.write(data) + + if self._line_start: + data = prefix + data + + self._line_start = data.endswith('\n') + # Append our prefix just after all inner newlines. Don't append to a + # trailing newline as we don't know if the next line in the log will be + # from this zone. + data = data.replace('\n', '\n' + prefix) + if self._line_start: + data = data[:-len(prefix)] + + return self.stream.write(data) + + def flush(self) -> None: + """ + Flushes the stream. + + This method flushes the stream to ensure that all buffered data is written + to the underlying file or device. + """ + self.stream.flush() + self.stream.flush() + + +def prefix_and_tee_streams(name: Path, prefix: Callable[[], str] | str | None = None) -> None: + """ + Tee stdout and stderr also to the named log file. + + Note: we intentionally don't provide a way to clean up the stream + replacement so that any error handling from Python which causes us to exit + is also captured by the log file. + """ + log_file = name.open(mode='w') + + sys.stdout = InsertPrefix( + Tee( + sys.stdout, + log_file, + ), + prefix=prefix, + ) + sys.stderr = InsertPrefix( + Tee( + sys.stderr, + log_file, + ), + prefix=prefix, + ) + + +def get_match_identifier() -> str: + """ + Get the identifier for this run of the simulator. + + This identifier is used to name the log files. + + :return: The match identifier + """ + match_data = get_match_data() + + if match_data.match_number is not None: + return f"match-{match_data.match_number}" + else: + return DATE_IDENTIFIER diff --git a/simulator/modules/robot_utils.py b/simulator/modules/robot_utils.py new file mode 100644 index 0000000..4cb02f5 --- /dev/null +++ b/simulator/modules/robot_utils.py @@ -0,0 +1,120 @@ +"""General utilities that are useful across runners.""" +from __future__ import annotations + +import json +import platform +import subprocess +import sys +from pathlib import Path +from typing import NamedTuple + +# Configure path to import the environment configuration +sys.path.insert(0, str(Path(__file__).parents[1])) +import environment + +# Reset the path +del sys.path[0] + + +class MatchData(NamedTuple): + """ + Data about the current match. + + :param match_number: The current match number + :param match_duration: The duration of the match in seconds + :param video_enabled: Whether video recording is enabled + :param video_resolution: The resolution of the video recording + """ + + match_number: int | None = None + match_duration: int = environment.DEFAULT_MATCH_DURATION + video_enabled: bool = True + video_resolution: tuple[int, int] = (1920, 1080) + + +def get_robot_file(robot_zone: int) -> Path: + """ + Get the path to the robot file for the given zone. + + :param robot_zone: The zone number + :return: The path to the robot file + :raises FileNotFoundError: If no robot controller is found for the given zone + """ + robot_file = environment.ZONE_ROOT / f'zone_{robot_zone}' / 'robot.py' + + # Check if the robot file exists + if not robot_file.exists(): + raise FileNotFoundError(f"No robot code to run for zone {robot_zone}") + + return robot_file + + +def get_game_mode() -> str: + """ + Get the game mode from the game mode file. + + Default to 'dev' if the file does not exist. + + :return: The game mode + """ + if environment.GAME_MODE_FILE.exists(): + game_mode = environment.GAME_MODE_FILE.read_text().strip() + else: + game_mode = 'dev' + + assert game_mode in ['dev', 'comp'], f'Invalid game mode: {game_mode}' + + return game_mode + + +def print_simulation_version() -> None: + """ + Print the version of the simulator that is running. + + Uses a VERSION file in the root of the simulator to determine the version. + For development, the version is uses the git describe command. + + The version is printed to the console. + """ + version_file = environment.SIM_ROOT / 'VERSION' + if version_file.exists(): + version = version_file.read_text().strip() + else: + try: + version = subprocess.check_output( + ['git', 'describe', '--tags', '--always'], + cwd=str(environment.SIM_ROOT.resolve()), + ).decode().strip() + except subprocess.CalledProcessError: + version = 'unknown' + + print( + f"Running simulator version: {version} in Python {platform.python_version()} " + f"({platform.system()}-{platform.machine()})" + ) + + +def get_match_data() -> MatchData: + """Load the match data from the match data file.""" + match_data_file = environment.ARENA_ROOT / 'match.json' + default_match_data = MatchData() + + if match_data_file.exists(): + # TODO error handling for invalid json + raw_data = json.loads(match_data_file.read_text()) + match_data = MatchData( + match_number=raw_data.get('match_number', default_match_data.match_number), + match_duration=raw_data.get('duration', default_match_data.match_duration), + video_enabled=( + raw_data.get('recording_config', {}) + .get('enabled', default_match_data.video_enabled) + ), + video_resolution=( + raw_data.get('recording_config', {}) + .get('video_resolution', default_match_data.video_resolution) + ), + ) + else: + match_data = default_match_data + + return match_data diff --git a/simulator/modules/sbot_interface/__init__.py b/simulator/modules/sbot_interface/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/simulator/modules/sbot_interface/__pycache__/__init__.cpython-313.pyc b/simulator/modules/sbot_interface/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..afafb84725abdeb5465d34941c300d9d8469ddb8 GIT binary patch literal 180 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 OrU1k6pAgLHq5U7hg7lRD literal 0 HcmV?d00001 diff --git a/simulator/modules/sbot_interface/boards/__init__.py b/simulator/modules/sbot_interface/boards/__init__.py new file mode 100644 index 0000000..3b2f426 --- /dev/null +++ b/simulator/modules/sbot_interface/boards/__init__.py @@ -0,0 +1,22 @@ +""" +A set of board simulators for the SRO robot. + +When connected to the SocketServer, these can be used with the sr-robot3 project. +""" +from sbot_interface.boards.arduino import Arduino +from sbot_interface.boards.camera import CameraBoard +from sbot_interface.boards.led_board import LedBoard +from sbot_interface.boards.motor_board import MotorBoard +from sbot_interface.boards.power_board import PowerBoard +from sbot_interface.boards.servo_board import ServoBoard +from sbot_interface.boards.time_server import TimeServer + +__all__ = [ + 'Arduino', + 'CameraBoard', + 'LedBoard', + 'MotorBoard', + 'PowerBoard', + 'ServoBoard', + 'TimeServer', +] diff --git a/simulator/modules/sbot_interface/boards/__pycache__/__init__.cpython-313.pyc b/simulator/modules/sbot_interface/boards/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b38111d8b9adda9888404fd7198a43b70e49591e GIT binary patch literal 869 zcmZ{izmC&D5XRSz6Fbi3pg>e0_BtXF`z|0vfshV5oBu{aqFFii1{-W|G`rz+boU4p zJOod{dsw=PiVldNW@nSjHOb2UJhT3J_M4HH!=X>~`gicX_+k_C1CxuTbF-GYetabf z0g_mW9b1tNHpRFTcOnNI6FYG?>Os%M-Pnyh@J!r``;ia6iQRY*?ZKXjy?7Yy!#*V; zN&2rWxbof~4L+aw$5e7f#f(lx3OSW!y{J+pK+>52stQh%*DolDsZfFMzb$w}Gto3W zQ#_|itO${Pdkp!a zY=jO6Ptux0`WOxA)O*VFi`aiAw6{(6Qk?M13=N3QyEkRcSC&RDbo3$Mpmb4sC@zYJ z(%15QbhULE%SFamdsFImS=lHKvozzu6s^cW3tBYQ+8f)M%y>6vwYRn-70;QeXw0++ z+o>8mU32Jn7dOWltT}9H7bkH#YYw~K#VI{EhA;;+7@SI4Rg8TjOZ^#X--U&!c_>eL z9;$jCqW$sazZp*sC-;NH;3(W|g|)~R6_*yJ#Xf@T`Y21^ l^0t;=9m}$Q+m7Y@^@%k(C-=8i@G>$ zv*%gL!oT~xd(1;UV_xbV^HCp1uCkLrZF|{6t_F@W)+h~jkUfO=Bna<4>axc|c{b@= zSPBkvnp`XwL|vua4E*$2iQ^}^VVWz;ipou^BF&|O!3nC)$vH{mM6M`lnwXckl1McP zH;B4J1Lkr6v@|Qu$v{k<;Y8)XXq^@{DaWabxz;&o?r^G)n~`bpnnSyi<0By0F^^MtGzp^=Hvi_rM4nv;x3rl3<%Qx!SOONyq_taVGPX01PFV*nbr z$yGubV6cm4fT1qx=KXwtXA|Td_d5*r@Il^M)|Kk1)uV1h=(Z0%n_} zWQ~)Rob)bio1Qnf!AYD_E>44lL?y?aEtd3oU>64~%gBC^b#N;wuB>bxjWAO|8?&fs zk}l|Cet^UC%q|CA1W2WlO7+IWafE4OwFzkIjE+(j<|L|tAT2u1C})p8om|$|5>uS` zjzO@@#(q<%mm>vp7^}-1O2#Ue`wI4pCnOIK3g3_$uV(}r_c9_576)O{ZMZe5FhfH? zg@&DH0rS)Pdtxc=?p@t$II^1{o)l^@qKiwEg7u8(3AZg}G zK~{8$&WKqlZE{LWgOZ`TrAqULUw~;@7X%|>voqz4bRmXTsIHUMwvP7$tMNVWH?MMi zpZmKvV3cGVSA`B@H5t_5!B?MQcm^7TcMV*IxBEIF88X>m6HM3z*0GL}P+S=o?>5)N z%DiXF)+BT9R$(}MU3kKz&!M&Xf7EU|CaHH5kN1V~_}~UlMiJON-Dx=}@X25ifnU78 zm+-zVczhzDLlc4w5{+B1I>|qupuA+|Ly#cBaKJS51Z$oea`zIe9HY% zeYTaUW@q4W4(zemn!W{WsBhcKW3e@V3)tGW0fX*iv9*}k)Mt_@`$W4CY1jpKhPCWM z&I>f%TW!P>bkdEMLGet*9K;#VREN`c=r;5+PRJHp_qLF}4BEi=j^Xy6 zG8oxlRD&rQO!1qcPeE$afn7OW`3V1X&7KQw}QhzCSPEU0-S zgzcQ1mvymVgfLKWI)3bc*OMrU-tdf$Ph>6`-iw(__#ZaA!bIl6h0KHzlr`XwE-G1x z!iyqC*dl8Wq47w1DcTFeXJ6C^Zow8Pl?$4LXbr!VomI`&j`i;zNsLt1a=R$y&><)mUOJ+J002VE)$pokTTuupT>BjUB7Sj@P0CmGHo7Q`^n& z-FSQHNUf>oe^OaZ^!zIMVe-ycHIc3-PE->o>WQIhV(8|PO8DHLuzE&Pk+u&4w*pHC zs*!#5NPjibe{bJPXo{QRzHb?>t`ZJYMTO zQHu>$B7^s1iAUtHvwJC5?>SQKIa29?kL-c!=QuOWyX1LgTmh? zQ>gZ4$uN#>geZ)-E_97TqEsvuB((N+mTcRP0gkrA5{hnlVb`cK zCl+MW^gEdtmFA4#_r*e4I!mc)S>B|_ zw1E_5X?VdN=o>>XM0lIO&HGV6#=v#NW1MO?^uBEp=>iU$ud*=84 zYP@f8Y&}TAO^XYkhYtMm%pEr>!AIjOp#!VYZqS2umV{q^=p{{2i}2v`Tg#)B&T}i_ z5g`1}hdW3lu|Zrw*ExF2ioZs8u6tnaUmE7#e~6v-kk34OP9I=DOEOR=Su0I56E^f0 ztV4h^Wr%11AVn);_`r+j@x^@ybJ)$yG-ZE~NZD^(u=^CWl9(>t z^3(zF)E9t+MCmD?4D+SS%eep1Oqji2 nkmkRUD^+skejrj0B>oaeEWK6>^j`OV?G+fN{qF>8lj?r}2Zmvg literal 0 HcmV?d00001 diff --git a/simulator/modules/sbot_interface/boards/__pycache__/camera.cpython-313.pyc b/simulator/modules/sbot_interface/boards/__pycache__/camera.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1901458d8d94333376ce0418ccdfa8c1efd09791 GIT binary patch literal 3584 zcmcInPf#1z8Q;~e{sR(VEFz<1v2n(tg9HYGOf04I zN}chkQzmJHJL54qHHRGFl(d&Prz9u0r!EmQXf{50CX)k~PEw}HwcmTYD-=%{r-!~l z?|tuk-}k-m`@Of{_c&;74p7j(>;7~l3)EM5;xyJG=z|p?9#TAoD4ynB)AZ{uR zOf!ffBa2uV*=hId9^~Pun@qaJF>^Scq;C3#X={%BozzK+cSkASGwiZ?fjkrUR{j=9 zahhB#7erk}+%o+16^Y~1bKJOCl#s~DiY}pLF)JkkfnOnYRnAEoCvrte)5N^Ql|-aT zSV`0!JFt#zEJ-Wkstg9zWstY5B?429IS^8Te?wYQbuBFwRb8@Vc(shO66{N?Xs8dV zATkI$W#51u6M=mkK%6#%qN1p}sLQINU4?mIOw?=}St~}XXRLuf=m+8{9?*to_fBi3@jRHhsk;@V| zN4a@`#-cfO>9&;BF%ET*xjVAHk_gy3qNYi@po@7+jZ8V1VihEnRHWBW%r?1%oviC? z>au=EM3S&7Aq`-%%s5=m=DyHc*1ntsM|^z%QTpHvL34pC5H{`>`V01j13Fx=FB(PV z;k2&C8Pi{n#|%48IIfu+A-nkyG1jYtZ0q>}_HEop6=D6N@30aj&Q5X@uad-YJS& z^w>&N8iV{?<9$c%J?L_L<{It#vE4~poCeGI$NX6ea^X45LYI1!E!GLJcwvKDH7c@4e8+w*R+};449`4;<#(aj&q8IH8i3jylxg z2u14{@5->l@D<^91VS-uwXt{3t`C?!RK9~ho}mF3R}U4BRG#2UmW2w&iCk7K7DXk; zX*!aXJcmgn>1AY0X2K^*+yvS%k^v8<~_ zs7+b1P*?+PrwUlR*=68|TSHO@qu4D-;Fb@ToZ|#e)MN`wFA*^5QG_!@#oaq~>FUKw zC$E=tlA?2ITr%aXHaeb~T^$fdE52TSJ~f}=FPcnhe7fT4O;7L>^OZ} zs#69rSk_LJAb2u2ld+V~ILZ*RS|}4-e_YLTIh~rF=&qb{G~^^aDXsdFyk;d(*vUAtZuUSbz}PRDhVX+Mor+f>9u_`SrO$m zGti*wm5X@+cGmFck4qzma0WD6R^Sd7B*D>)HWsrVuDuHK+BO{Ged?PwDja$D-uk^w zz8dQO16vD*Ht6+{jeIp2t9A5kz(+kFC;pl+22w`n*lyE!gPeP}IrcQvwlVbf z+UCI9_iB-$XQB3u)cT#xL^X8gY54e~;Dey?^331o|9R^lw~U#aM%Qn4!?$XY=%c$2 z?tZ|0Furx{!xP&l_Traz;+LxN%T;c&8o6SGuhd#QHpB;!&Dm;eyl&_6XZn-3K7Y%& zeBJ1}u^V2f#k&8p_}=1PY-A@kQjMM82-KoI?_YbbZ}XKcdaL)NYukNWuNaZ@M)#m zZ~?srJe=&bv(SCR&@}z6%T2TYX`<*;Ur{YzQq5meL(lx-J%98Ie{}O))!%d9^Zl_w M`t-jk=!nh#0_S9NmjD0& literal 0 HcmV?d00001 diff --git a/simulator/modules/sbot_interface/boards/__pycache__/led_board.cpython-313.pyc b/simulator/modules/sbot_interface/boards/__pycache__/led_board.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..58871542aa6e1eab92e47f85077d77e85eda3b76 GIT binary patch literal 4900 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 Nk)rnh3z1rt{C~jNb3*_C literal 0 HcmV?d00001 diff --git a/simulator/modules/sbot_interface/boards/__pycache__/motor_board.cpython-313.pyc b/simulator/modules/sbot_interface/boards/__pycache__/motor_board.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..11ffc234272089a031030c8499eed8435c5742a5 GIT binary patch literal 4900 zcma)AT~Hg>6~3$0KOh7Wh<_H0GZA%!F!(33z+giN#yAO!Vo!0bS)@glBkhvA3$~S? zX8O>nCz%PJjB7IPOvqF5G&6DY689->r!#HdL`4Q=hX>E3Gp*l9ahm?T^xV6j@ z1|7LO?qNCBhAJnY5x3RT+i=^-z^5Ttb#4F`>eMDpP&Yan4XBxA&O_ z)l*q%g-c6G@ftABUN2*m7P7M<4!I2kf})6OToqCgj3K1o=@aD8eg;+$YHX8UPstqa}^9VV{}T*h!`=!Y|g8zYoL zcNyXpc3o$htF$Z4Yhp1G2CbS^5i@hR5(MHZ@`8hi6as-LQ+uW7vYZurlSMpWp`cv@0LJSQZ?9%>>5sBnhjy1nMFYtA@K zNvrX==9?rG>OdiLu$e&d!|QFdRfz(v?^YBWJKyybx$u|H)@>L^iPEm&!wLUXSAPfi zw@{3kD(Q#J)gPWQgMeg-S$K;LhIuw}0bI5Pde#`4Dwk<+c_82brQ9+lN}sh0l_c4y zBv?0PFO50q7#z15yTOE}TxM^l<(-dPyU$``<`B%GL34+X^nB=fA3_ovW2cA__dG|8 zZw^h?Ayk1llya5Oxe2;4M)(!rt2P;9r4{*#NG<4w?5MI0&5*NAAK{@9DcWsHow+DCi zF5I>m;$ifO*-H2;Kx(}?PIhD<^aDcw6YN+Qvx2|WKhBzbAZGKRKD)X9$JsPyB{-)V zO%ywA%(lZzEvf6Hf$C!#yqK9HM`zDdX1jguVXW7>hYdbW|1nqfJjM$R3GI)U;6Xhl z3(-@fBX6Y0PlL{X_=Gh5ux9(LCc-D^^asyKnMRu_F0jB%hq))P>H)LA)9%$!7fQA8 ztq_3@?#wZJ=3MXrOBd=uGvNRF<3Iw$FAlZCDVRe8Y&)7`9z*w=^k7?VjnpCus6jXZ z5@a?jNJ&mnaau}oq>Dv$0qfNT7L|FZ8K9JKk6sfK3#w6`2=nt;p2ulHg`}4tbp@$M z1nD}b%!`TiTzV%13k#~8h0-G-WHQ%5j*qdD(LS`lV5JNw94#!ngmW74aVd{r`3VF1k4QCKF;G+fZ9 z*_8QATHPrLH2c{X#>Sw+w5BDfib=hoIg)faQDZZrq_L3fHTTOxW_oP{`$Z1J)h!WrwlbS14qOjnznj^7*F<2Vb2M?YP+A+yVFs%EvW)*Ns(QMS7 zTBTl1=*G}&bOD7#5U5<>!Q>X8c`m~t$cS-MLStE>v71EBs&WN<0?LhALupJv87a@qP19eXm$FNOLs5j4@K_}-%oA~@@s>9eqcK9i{&e0#j2X6 zjvMEf-G!>o2fjA*(^;tEHY;o2o47HNZwlY@f7bS!wtRRf-!Qyhd8*jh`eE}go0n`w zZ{6*xn^nK4{)Rj_T@{TfX!k|Cn7J`DEhmMB(Y<>;B_pGK=b-h4!!IBKnHezNO(CSCzG7W@=_ALTvrD6o?5MGGCH7^*v5(X14adkF7BwD+ zcGFvbyW(a=vA%2N;>x*u-S?+Izx3Nn`RLg{kK|u^IsfAMy#K-@)>2ao_-g9N)aA2l z-r$C}Z_V3xZ*<)oDF#}KHFZnE&A{?=g__P{py~Fto7Xl2P0Rfsp7`Lz${TCV{Tt2E zwdQD{d9V;TnXexz2AXfbe)IK}=vpAW5s0h>B85OSUq7&AV>@cM5!>L~LTrt1%ZY01 zso?QK%|QcczAd!+#;SDx%h--EjD)nlIw+#UG!$$a;Tyf2!sjQ;D<7=!$6+lZ;^`--R^+W$Uk zKnHchP=Dn_Il(<9s`Ngj!i6%!8K;j+V&N*it7bCB z)r*As^4R^P`EcLjvcn#B1|-P&-(5{td2BG$TVkQwBq& zX|ECqP{g8#5DoIRL!VBp9@+49t@*k(d{5md3;&Lj#sV zOri%M+znl=_WP@fWC>E;y^S7KBCl_)cIe*7XA_@J6l#YG)kAsb5Qdr_1z9}^(;FJO zoSh3YIUD_H0HPxq%~g{-?o8>r|BB8K1QNZ?VbTj)@gXum`kGz0q)xEl6NGSpOmdMM znIvRIxRJTzas4g_{mOX!wFMz#P^#l`_)$VpA<>AE3}i<p6z$YQq*GrCkFFj$F)pdnkBTY%~Zwu6c9X^xDXNHeP(n1L(4?iOFOp4Yk~#~FBF0hp#-za2fuABhVkJG-IHS782Jsk- z5g$;F!VAti!+c}0GuCfCh&k{z^85`wzlNTF;PP&`n*QQyT0UNIb-iu>&i-SD8D{>0 IXhY@x4<&+tumAu6 literal 0 HcmV?d00001 diff --git a/simulator/modules/sbot_interface/boards/__pycache__/power_board.cpython-313.pyc b/simulator/modules/sbot_interface/boards/__pycache__/power_board.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..998e8acdfa57a9ff487c52f9968e694f1c8693ad GIT binary patch literal 8315 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 literal 0 HcmV?d00001 diff --git a/simulator/modules/sbot_interface/boards/__pycache__/servo_board.cpython-313.pyc b/simulator/modules/sbot_interface/boards/__pycache__/servo_board.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..65f8cd30245538678d6527c5f7733403ed47cfd7 GIT binary patch literal 4927 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 str: + """ + Process a command string and return the response. + + Executes the appropriate action on any specified pins automatically. + + :param command: The command string to process. + :return: The response to the command. + """ + if command[0] == 'a': # analog read + pin_number = self._convert_pin_number(command, 1) + if pin_number == -1: + return '0' + return str(self.pins[pin_number].get_analog()) + elif command[0] == 'r': # digital read + pin_number = self._convert_pin_number(command, 1) + if pin_number == -1: + return 'l' + return 'h' if self.pins[pin_number].get_digital() else 'l' + elif command[0] == 'l': # digital write low + pin_number = self._convert_pin_number(command, 1) + if pin_number != -1: + self.pins[pin_number].set_digital(False) + return '' + elif command[0] == 'h': # digital write high + pin_number = self._convert_pin_number(command, 1) + if pin_number != -1: + self.pins[pin_number].set_digital(True) + return '' + elif command[0] == 'i': # set pin mode to input + pin_number = self._convert_pin_number(command, 1) + if pin_number != -1: + self.pins[pin_number].set_mode(GPIOPinMode.INPUT) + return '' + elif command[0] == 'o': # set pin mode to output + pin_number = self._convert_pin_number(command, 1) + if pin_number != -1: + self.pins[pin_number].set_mode(GPIOPinMode.OUTPUT) + return '' + elif command[0] == 'p': # set pin mode to input pullup + pin_number = self._convert_pin_number(command, 1) + if pin_number != -1: + self.pins[pin_number].set_mode(GPIOPinMode.INPUT_PULLUP) + return '' + elif command[0] == 'u': # ultrasonic measurement + pulse_pin = self._convert_pin_number(command, 1) + echo_pin = self._convert_pin_number(command, 2) + + if pulse_pin == -1 or echo_pin == -1: + return '0' + + ultrasound_sensor = self.pins[echo_pin] + if isinstance(ultrasound_sensor, UltrasonicSensor): + return str(ultrasound_sensor.get_distance()) + else: + return '0' + elif command[0] == 'v': # software version + return f"SRduino:{self.software_version}" + else: + # A problem here: we do not know how to handle the command! + # Just ignore this for now. + return '' + + def _convert_pin_number(self, command: str, index: int) -> int: + if len(command) < index + 1: + LOGGER.warning(f'Incomplete arduino command: {command}') + return -1 + + pin_str = command[index] + try: + pin_number = ord(pin_str) - ord('a') + except ValueError: + LOGGER.warning(f'Invalid pin number in command: {command}') + return -1 + + if 0 < pin_number < len(self.pins): + return pin_number + else: + LOGGER.warning(f'Invalid pin number in command: {command}') + return -1 diff --git a/simulator/modules/sbot_interface/boards/camera.py b/simulator/modules/sbot_interface/boards/camera.py new file mode 100644 index 0000000..b8391b9 --- /dev/null +++ b/simulator/modules/sbot_interface/boards/camera.py @@ -0,0 +1,75 @@ +""" +A simulator for the SRO Camera interface. + +Provides a message parser that simulates the behavior of a Camera. +Interfaces to a WebotsRemoteCameraSource in the sr-robot3 package. +""" +from __future__ import annotations + +import logging +import struct + +from sbot_interface.devices.camera import BaseCamera + +LOGGER = logging.getLogger(__name__) + +# *IDN? +# *STATUS? +# *RESET +# CAM:CALIBRATION? +# CAM:RESOLUTION? +# CAM:FRAME! + + +class CameraBoard: + """ + A simulator for the SRO Camera interface. + + :param camera: The camera object to interface with. + :param asset_tag: The asset tag to report for the camera board. + :param software_version: The software version to report for the camera board. + """ + + def __init__(self, camera: BaseCamera, asset_tag: str, software_version: str = '1.0'): + self.asset_tag = asset_tag + self.software_version = software_version + self.camera = camera + + def handle_command(self, command: str) -> str | bytes: + """ + Process a command string and return the response. + + Executes the appropriate action on the camera automatically. + + :param command: The command string to process. + :return: The response to the command. + """ + args = command.split(':') + if args[0] == '*IDN?': + return f'Student Robotics:CAMv1a:{self.asset_tag}:{self.software_version}' + elif args[0] == '*STATUS?': + return 'ACK' + elif args[0] == '*RESET': + LOGGER.info(f'Resetting camera board {self.asset_tag}') + return 'ACK' + elif args[0] == 'CAM': + if len(args) < 2: + return 'NACK:Missing camera command' + + if args[1] == 'CALIBRATION?': + LOGGER.info(f'Getting calibration data from camera on board {self.asset_tag}') + return ':'.join(map(str, self.camera.get_calibration())) + elif args[1] == 'RESOLUTION?': + LOGGER.info(f'Getting resolution from camera on board {self.asset_tag}') + resolution = self.camera.get_resolution() + return f'{resolution[0]}:{resolution[1]}' + elif args[1] == 'FRAME!': + LOGGER.info(f'Getting image from camera on board {self.asset_tag}') + resolution = self.camera.get_resolution() + img_len = resolution[0] * resolution[1] * 4 # 4 bytes per pixel + return struct.pack('>BI', 0, img_len) + self.camera.get_image() + else: + return 'NACK:Unknown camera command' + else: + return f'NACK:Unknown command {command}' + return 'NACK:Command failed' diff --git a/simulator/modules/sbot_interface/boards/led_board.py b/simulator/modules/sbot_interface/boards/led_board.py new file mode 100644 index 0000000..58c544d --- /dev/null +++ b/simulator/modules/sbot_interface/boards/led_board.py @@ -0,0 +1,117 @@ +""" +A simulator for the SRO LED hat. + +Provides a message parser that simulates the behavior of the LED hat. +""" +from __future__ import annotations + +import logging + +from sbot_interface.devices.led import RGB_COLOURS, BaseLed + +LOGGER = logging.getLogger(__name__) + +# *IDN? +# *STATUS? +# *RESET +# LED::SET::: +# LED::GET? +# LED:START:SET:<0/1> +# LED:START:GET? + +LED_START = 4 + + +class LedBoard: + """ + A simulator for the SRO LED hat. + + :param leds: A list of simulated LEDs connected to the LED hat. + The list is indexed by the LED number. + :param asset_tag: The asset tag to report for the LED hat. + :param software_version: The software version to report for the LED hat. + """ + + def __init__(self, leds: list[BaseLed], asset_tag: str, software_version: str = '1.0'): + self.leds = leds + self.asset_tag = asset_tag + self.software_version = software_version + + def handle_command(self, command: str) -> str: + """ + Process a command string and return the response. + + Executes the appropriate action on any specified LEDs automatically. + + :param command: The command string to process. + :return: The response to the command. + """ + args = command.split(':') + if args[0] == '*IDN?': + return f'Student Robotics:KCHv1B:{self.asset_tag}:{self.software_version}' + elif args[0] == '*STATUS?': + return 'ACK' + elif args[0] == '*RESET': + LOGGER.info(f'Resetting led board {self.asset_tag}') + for led in self.leds: + led.set_colour(0) + return 'ACK' + elif args[0] == 'LED': + if len(args) < 2: + return 'NACK:Missing LED number' + + if args[1] == 'START': + led_number = LED_START + if len(args) < 3: + return 'NACK:Missing LED command' + if args[2] == 'SET': + if len(args) < 4: + return 'NACK:Missing LED start' + try: + start = int(args[3]) + except ValueError: + return 'NACK:Invalid LED start' + if start not in [0, 1]: + return 'NACK:Invalid LED start' + LOGGER.info(f'Setting start LED on board {self.asset_tag} to {start}') + self.leds[led_number].set_colour(start) + return 'ACK' + elif args[2] == 'GET?': + return str(self.leds[led_number].get_colour()) + else: + return "NACK:Unknown start command" + else: + try: + led_number = int(args[1]) + except ValueError: + return 'NACK:Invalid LED number' + if not (0 <= led_number < len(self.leds)): + return 'NACK:Invalid LED number' + + if len(args) < 3: + return 'NACK:Missing LED command' + if args[2] == 'SET': + if len(args) < 6: + return 'NACK:Missing LED colour' + try: + r = bool(int(args[3])) + g = bool(int(args[4])) + b = bool(int(args[5])) + except ValueError: + return 'NACK:Invalid LED colour' + if r not in [0, 1] or g not in [0, 1] or b not in [0, 1]: + return 'NACK:Invalid LED colour' + LOGGER.info( + f'Setting LED {led_number} on board {self.asset_tag} to ' + f'{r:d}:{g:d}:{b:d} (colour {RGB_COLOURS.index((r, g, b))}', + ) + self.leds[led_number].set_colour(RGB_COLOURS.index((r, g, b))) + return 'ACK' + elif args[2] == 'GET?': + colour = RGB_COLOURS[self.leds[led_number].get_colour()] + return f"{colour[0]:d}:{colour[1]:d}:{colour[2]:d}" + else: + return 'NACK:Unknown LED command' + else: + return f'NACK:Unknown command {command.strip()}' + return 'NACK:Command failed' diff --git a/simulator/modules/sbot_interface/boards/motor_board.py b/simulator/modules/sbot_interface/boards/motor_board.py new file mode 100644 index 0000000..6a2b2be --- /dev/null +++ b/simulator/modules/sbot_interface/boards/motor_board.py @@ -0,0 +1,107 @@ +""" +A simulator for the SRv4 Motor Board. + +Provides a message parser that simulates the behavior of a motor board. + +Based on the Motor Board v4.4.1 firmware. +""" +from __future__ import annotations + +import logging + +from sbot_interface.devices.motor import MAX_POWER, MIN_POWER, BaseMotor + +LOGGER = logging.getLogger(__name__) + + +class MotorBoard: + """ + A simulator for the SRv4 Motor Board. + + :param motors: A list of simulated motors connected to the motor board. + The list is indexed by the motor number. + :param asset_tag: The asset tag to report for the motor board. + :param software_version: The software version to report for the motor board. + """ + + def __init__( + self, + motors: list[BaseMotor], + asset_tag: str, + software_version: str = '4.4.1' + ): + self.motors = motors + self.asset_tag = asset_tag + self.software_version = software_version + + def handle_command(self, command: str) -> str: + """ + Process a command string and return the response. + + Executes the appropriate action on any specified motors automatically. + + :param command: The command string to process. + :return: The response to the command. + """ + args = command.split(':') + if args[0] == '*IDN?': + return f'Student Robotics:MCv4B:{self.asset_tag}:{self.software_version}' + elif args[0] == '*STATUS?': + # Output faults are unsupported + return "0,0:12000" + elif args[0] == '*RESET': + LOGGER.info(f'Resetting motor board {self.asset_tag}') + for motor in self.motors: + motor.disable() + return 'ACK' + elif args[0] == 'MOT': + if len(args) < 2: + return 'NACK:Missing motor number' + + try: + motor_number = int(args[1]) + except ValueError: + return 'NACK:Invalid motor number' + if not (0 <= motor_number < len(self.motors)): + return 'NACK:Invalid motor number' + + if len(args) < 3: + return 'NACK:Missing motor command' + if args[2] == 'SET': + if len(args) < 4: + return 'NACK:Missing motor power' + try: + power = int(args[3]) + except ValueError: + return 'NACK:Invalid motor power' + if not (MIN_POWER <= power <= MAX_POWER): + return 'NACK:Invalid motor power' + LOGGER.info( + f'Setting motor {motor_number} on board {self.asset_tag} to {power}' + ) + self.motors[motor_number].set_power(power) + return 'ACK' + elif args[2] == 'GET?': + return ':'.join([ + f'{int(self.motors[motor_number].enabled())}', + f'{self.motors[motor_number].get_power()}', + ]) + elif args[2] == 'DISABLE': + LOGGER.info(f'Disabling motor {motor_number} on board {self.asset_tag}') + self.motors[motor_number].disable() + return 'ACK' + elif args[2] == 'I?': + return str(self.current()) + else: + return 'NACK:Unknown motor command' + else: + return f'NACK:Unknown command {command.strip()}' + return 'NACK:Command failed' + + def current(self) -> int: + """ + Get the total current draw of all motors. + + :return: The total current draw of all motors in mA. + """ + return sum(motor.get_current() for motor in self.motors) diff --git a/simulator/modules/sbot_interface/boards/power_board.py b/simulator/modules/sbot_interface/boards/power_board.py new file mode 100644 index 0000000..0d51a8e --- /dev/null +++ b/simulator/modules/sbot_interface/boards/power_board.py @@ -0,0 +1,192 @@ +""" +A simulator for the SRv4 Power Board. + +Provides a message parser that simulates the behavior of a power board. + +Based on the Power Board v4.4.2 firmware. +""" +from __future__ import annotations + +import logging + +from sbot_interface.devices.led import BaseLed +from sbot_interface.devices.power import BaseButton, BaseBuzzer, Output + +LOGGER = logging.getLogger(__name__) + +NUM_OUTPUTS = 7 # 6 12V outputs, 1 5V output +SYS_OUTPUT = 4 # L2 output for the brain + +RUN_LED = 0 +ERR_LED = 1 + + +class PowerBoard: + """ + A simulator for the SRv4 Power Board. + + :param outputs: A list of simulated outputs connected to the power board. + The list is indexed by the output number. + :param buzzer: A simulated buzzer connected to the power board. + :param button: A simulated button connected to the power board. + :param leds: A tuple of simulated LEDs connected to the power board. + :param asset_tag: The asset tag to report for the power board. + :param software_version: The software version to report for the power board. + """ + + def __init__( + self, + outputs: list[Output], + buzzer: BaseBuzzer, + button: BaseButton, + leds: tuple[BaseLed, BaseLed], + asset_tag: str, + software_version: str = '4.4.2', + ): + self.outputs = outputs + self.buzzer = buzzer + self.button = button + self.leds = leds + self.asset_tag = asset_tag + self.software_version = software_version + self.temp = 25 + self.battery_voltage = 12000 + + def handle_command(self, command: str) -> str: + """ + Process a command string and return the response. + + Executes the appropriate action on any specified outputs, LEDs, + or the buzzer automatically. + + :param command: The command string to process. + :return: The response to the command. + """ + args = command.split(':') + if args[0] == '*IDN?': + return f'Student Robotics:PBv4B:{self.asset_tag}:{self.software_version}' + elif args[0] == '*STATUS?': + # Output faults are unsupported, fan is always off + return f"0,0,0,0,0,0,0:{self.temp}:0:5000" + elif args[0] == '*RESET': + LOGGER.info(f'Resetting power board {self.asset_tag}') + for output in self.outputs: + output.set_output(False) + self.buzzer.set_note(0, 0) + self.leds[RUN_LED].set_colour(0) + self.leds[ERR_LED].set_colour(0) + return 'ACK' + elif args[0] == 'BTN': + if len(args) < 2: + return 'NACK:Missing button command' + if args[1] == 'START' and args[2] == 'GET?': + return f'{self.button.get_state():d}:0' + else: + return 'NACK:Unknown button command' + elif args[0] == 'OUT': + if len(args) < 2: + return 'NACK:Missing output number' + try: + output_number = int(args[1]) + except ValueError: + return 'NACK:Invalid output number' + if not (0 <= output_number < NUM_OUTPUTS): + return 'NACK:Invalid output number' + + if len(args) < 3: + return 'NACK:Missing output command' + if args[2] == 'SET': + if output_number == SYS_OUTPUT: + return 'NACK:Brain output cannot be controlled' + if len(args) < 4: + return 'NACK:Missing output state' + try: + state = int(args[3]) + except ValueError: + return 'NACK:Invalid output state' + if state not in [0, 1]: + return 'NACK:Invalid output state' + LOGGER.info( + f'Setting output {output_number} on board {self.asset_tag} to {state}' + ) + self.outputs[output_number].set_output(bool(state)) + return 'ACK' + elif args[2] == 'GET?': + return '1' if self.outputs[output_number].get_output() else '0' + elif args[2] == 'I?': + return str(self.outputs[output_number].get_current()) + else: + return 'NACK:Unknown output command' + elif args[0] == 'BATT': + if len(args) < 2: + return 'NACK:Missing battery command' + if args[1] == 'V?': + return str(self.battery_voltage) + elif args[1] == 'I?': + return str(self.current()) + else: + return 'NACK:Unknown battery command' + elif args[0] == 'LED': + if len(args) < 3: + return 'NACK:Missing LED command' + if args[1] not in ['RUN', 'ERR']: + return 'NACK:Invalid LED type' + + led_type = RUN_LED if args[1] == 'RUN' else ERR_LED + + if args[2] == 'SET': + if len(args) < 4: + return 'NACK:Missing LED state' + if args[3] in ['0', '1', 'F']: + LOGGER.info( + f'Setting {args[1]} LED on board {self.asset_tag} to {args[3]}' + ) + if args[3] == 'F': + self.leds[led_type].set_colour(1) + else: + self.leds[led_type].set_colour(int(args[3])) + return 'ACK' + else: + return 'NACK:Invalid LED state' + elif args[2] == 'GET?': + return str(self.leds[led_type].get_colour()) + else: + return 'NACK:Invalid LED command' + elif args[0] == 'NOTE': + if len(args) < 2: + return 'NACK:Missing note command' + if args[1] == 'GET?': + return ':'.join(map(str, self.buzzer.get_note())) + else: + if len(args) < 3: + return 'NACK:Missing note frequency' + try: + freq = int(args[1]) + except ValueError: + return 'NACK:Invalid note frequency' + if not (0 <= freq < 10000): + return 'NACK:Invalid note frequency' + + try: + dur = int(args[2]) + except ValueError: + return 'NACK:Invalid note duration' + if dur < 0: + return 'NACK:Invalid note duration' + + LOGGER.info( + f'Setting buzzer on board {self.asset_tag} to {freq}Hz for {dur}ms' + ) + self.buzzer.set_note(freq, dur) + return 'ACK' + else: + return f'NACK:Unknown command {command.strip()}' + return 'NACK:Command failed' + + def current(self) -> int: + """ + Get the total current draw of all outputs. + + :return: The total current draw of all outputs in mA. + """ + return sum(output.get_current() for output in self.outputs) diff --git a/simulator/modules/sbot_interface/boards/servo_board.py b/simulator/modules/sbot_interface/boards/servo_board.py new file mode 100644 index 0000000..0aa922e --- /dev/null +++ b/simulator/modules/sbot_interface/boards/servo_board.py @@ -0,0 +1,105 @@ +""" +A simulator for the SRv4 Servo Board. + +Provides a message parser that simulates the behavior of a servo board. + +Based on the Servo Board v4.4 firmware. +""" +from __future__ import annotations + +import logging + +from sbot_interface.devices.servo import MAX_POSITION, MIN_POSITION, BaseServo + +LOGGER = logging.getLogger(__name__) + + +class ServoBoard: + """ + A simulator for the SRv4 Servo Board. + + :param servos: A list of simulated servos connected to the servo board. + The list is indexed by the servo number. + :param asset_tag: The asset tag to report for the servo board. + :param software_version: The software version to report for the servo board. + """ + + def __init__(self, servos: list[BaseServo], asset_tag: str, software_version: str = '4.4'): + self.servos = servos + self.asset_tag = asset_tag + self.software_version = software_version + self.watchdog_fail = False + self.pgood = True + + def handle_command(self, command: str) -> str: + """ + Process a command string and return the response. + + Executes the appropriate action on any specified servos automatically. + + :param command: The command string to process. + :return: The response to the command. + """ + args = command.split(':') + if args[0] == '*IDN?': + return f'Student Robotics:SBv4B:{self.asset_tag}:{self.software_version}' + elif args[0] == '*STATUS?': + return f"{self.watchdog_fail:d}:{self.pgood:d}" + elif args[0] == '*RESET': + LOGGER.info(f'Resetting servo board {self.asset_tag}') + for servo in self.servos: + servo.disable() + return 'ACK' + elif args[0] == 'SERVO': + if len(args) < 2: + return 'NACK:Missing servo number' + if args[1] == 'I?': + return str(self.current()) + elif args[1] == 'V?': + return '5000' + + try: + servo_number = int(args[1]) + except ValueError: + return 'NACK:Invalid servo number' + if not (0 <= servo_number < len(self.servos)): + return 'NACK:Invalid servo number' + + if len(args) < 3: + return 'NACK:Missing servo command' + + if args[2] == 'DISABLE': + LOGGER.info(f'Disabling servo {servo_number} on board {self.asset_tag}') + self.servos[servo_number].disable() + return 'ACK' + elif args[2] == 'GET?': + return str(self.servos[servo_number].get_position()) + elif args[2] == 'SET': + if len(args) < 4: + return 'NACK:Missing servo setpoint' + + try: + setpoint = int(args[3]) + except ValueError: + return 'NACK:Invalid servo setpoint' + if not (MIN_POSITION <= setpoint <= MAX_POSITION): + return 'NACK:Invalid servo setpoint' + + LOGGER.info( + f'Setting servo {servo_number} on board {self.asset_tag} to {setpoint}' + ) + self.servos[servo_number].set_position(setpoint) + return 'ACK' + else: + return 'NACK:Unknown servo command' + else: + return f'NACK:Unknown command {command.strip()}' + return 'NACK:Command failed' + + def current(self) -> int: + """ + Get the total current draw of all servos. + + :return: The total current draw of all servos in mA. + """ + return sum(servo.get_current() for servo in self.servos) diff --git a/simulator/modules/sbot_interface/boards/time_server.py b/simulator/modules/sbot_interface/boards/time_server.py new file mode 100644 index 0000000..3dc0e57 --- /dev/null +++ b/simulator/modules/sbot_interface/boards/time_server.py @@ -0,0 +1,66 @@ +""" +A simulator for handling time based commands using simulated time. + +Provides a message parser that simulates the behavior of sleep and time. +""" +from __future__ import annotations + +import logging +from datetime import datetime, timedelta + +from sbot_interface.devices.util import get_globals + +LOGGER = logging.getLogger(__name__) +g = get_globals() + + +class TimeServer: + """ + A simulator for handling time based commands using simulated time. + + :param asset_tag: The asset tag to report for the time server. + :param software_version: The software version to report for the time server. + :param start_time: The start time for the time server (reported time to simulator time 0). + """ + + def __init__( + self, + asset_tag: str, + software_version: str = '1.0', + start_time: str = '2024-06-01T00:00:00+00:00', + ): + self.asset_tag = asset_tag + self.software_version = software_version + self.start_time = datetime.fromisoformat(start_time) + + def handle_command(self, command: str) -> str: + """ + Process a command string and return the response. + + :param command: The command string to process. + :return: The response to the command. + """ + args = command.split(':') + if args[0] == '*IDN?': + return f'SourceBots:TimeServer:{self.asset_tag}:{self.software_version}' + elif args[0] == '*STATUS?': + return "Yes" + elif args[0] == '*RESET': + return "NACK:Reset not supported" + elif args[0] == 'TIME?': + sim_time = g.robot.getTime() + current_time = self.start_time + timedelta(seconds=sim_time) + return current_time.isoformat('T', timespec='milliseconds') + elif args[0] == 'SLEEP': + if len(args) < 2: + return 'NACK:Missing duration' + try: + duration = int(args[1]) + except ValueError: + return 'NACK:Invalid duration' + LOGGER.info(f'Sleeping for {duration} ms') + g.sleep(duration / 1000) + return 'ACK' + else: + return f'NACK:Unknown command {command.strip()}' + return 'NACK:Command failed' diff --git a/simulator/modules/sbot_interface/devices/__init__.py b/simulator/modules/sbot_interface/devices/__init__.py new file mode 100644 index 0000000..33a317f --- /dev/null +++ b/simulator/modules/sbot_interface/devices/__init__.py @@ -0,0 +1 @@ +"""A set of wrappers for board simulators to interact with Webots devices.""" diff --git a/simulator/modules/sbot_interface/devices/__pycache__/__init__.cpython-313.pyc b/simulator/modules/sbot_interface/devices/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ca9d6c446cde67657491e67707eed78de2ac227d GIT binary patch literal 274 zcmX|6u}%U(5Zy&0#DsqsOA2n0!itbkSxIAIVX;|m=e$ig=dv?M@IU+rzrz2x)*mpr zgql4>rkVF%-ptEuN242+=5YI6uG0SL$p7dwXom@WswWlIs~-109+to;Kn3igt80u5 z1w~ksiyk;s+l>=Sq7Z~P!RT^?`I^}ZR-|ICYcowB-m Q+*SL%(pop_G4>NV0w_6BW&i*H literal 0 HcmV?d00001 diff --git a/simulator/modules/sbot_interface/devices/__pycache__/arduino_devices.cpython-313.pyc b/simulator/modules/sbot_interface/devices/__pycache__/arduino_devices.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7692752e9428f393fc4467e2f7c1c0c911f6b0cb GIT binary patch literal 12571 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 literal 0 HcmV?d00001 diff --git a/simulator/modules/sbot_interface/devices/__pycache__/camera.cpython-313.pyc b/simulator/modules/sbot_interface/devices/__pycache__/camera.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c6768109bdb5cf5a57d4dcca78508d523a58f739 GIT binary patch literal 5152 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

{fH!S3sS_&<~H#G1vddYFuP0GXikkf&uYm zVTFia!c#Cod0~1lXhBwF0e(!7-M2^RCFm7V!Tid-Im>|Rg&wsTKPC)dwzz1h# zN@j;)4hD-uWv&BlJg^{L4xy){sw_s#CWgEqb(0^-(Wouy>iQqG z&`ok(zfXRd)gSdeJEQNO(f^j!pXc%qXCBNv%MI`5h9BMjGB@_m-r=3I#V5(f_jg8q z^0fEuo!r|;gK8&^BfpLds&hzzLnZPU47lsR(T;zXfHA})@_-rk<99Ot^a$XwMPlT2 znF(+qrXNXh6z(2yJmZLe3v^yT2*5K>@_i3KdGN`zp0m3>XP@?*+ex3}P=&j4AZUSZR46e8vQ97IczB7 yk;eW-j9p^$y|NIYr`zT%j literal 0 HcmV?d00001 diff --git a/simulator/modules/sbot_interface/devices/__pycache__/motor.cpython-313.pyc b/simulator/modules/sbot_interface/devices/__pycache__/motor.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0fe83f659aef0b51d138688c68c5d607f90b8c02 GIT binary patch literal 6278 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 OH*n+auL$OBq5lQp|36m% literal 0 HcmV?d00001 diff --git a/simulator/modules/sbot_interface/devices/__pycache__/power.cpython-313.pyc b/simulator/modules/sbot_interface/devices/__pycache__/power.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6f26effe0bd9e0e39853cf5e29e490b21a1db21e GIT binary patch literal 7027 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 literal 0 HcmV?d00001 diff --git a/simulator/modules/sbot_interface/devices/__pycache__/servo.cpython-313.pyc b/simulator/modules/sbot_interface/devices/__pycache__/servo.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..06579a88f2f658280508e3fdb405a67003e22dd8 GIT binary patch literal 6124 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 YH}qDdN#LiM<(}n+R>waf@WWL3Up$opHvj+t literal 0 HcmV?d00001 diff --git a/simulator/modules/sbot_interface/devices/__pycache__/util.cpython-313.pyc b/simulator/modules/sbot_interface/devices/__pycache__/util.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..15f6ae76ed631149ed164ccce4b946452951c9dd GIT binary patch literal 5472 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 GPIOPinMode: + """Get the current mode of the pin.""" + pass + + @abstractmethod + def set_mode(self, mode: GPIOPinMode) -> None: + """Set the mode of the pin.""" + pass + + @abstractmethod + def get_digital(self) -> bool: + """Get the digital input value of the pin.""" + pass + + @abstractmethod + def set_digital(self, value: bool) -> None: + """Set the digital output value of the pin.""" + pass + + @abstractmethod + def get_analog(self) -> int: + """Get the analog input value of the pin.""" + pass + + +class EmptyPin(BasePin): + """A pin that does nothing. Used for pins that are not connected.""" + + def __init__(self) -> None: + self._mode = GPIOPinMode.INPUT + self._digital = False + self._analog = 0 + + def get_mode(self) -> GPIOPinMode: + """Get the current mode of the pin.""" + return self._mode + + def set_mode(self, mode: GPIOPinMode) -> None: + """Set the mode of the pin.""" + self._mode = mode + + def get_digital(self) -> bool: + """Get the digital input value of the pin.""" + return self._digital + + def set_digital(self, value: bool) -> None: + """Set the digital output value of the pin.""" + self._digital = value + + def get_analog(self) -> int: + """Get the analog input value of the pin.""" + return self._analog + + +class UltrasonicSensor(BasePin): + """ + A sensor that can measure the distance to an object. + + This is attached to the pin specified to be the echo pin, with the trigger pin unused. + """ + + def __init__(self, device_name: str) -> None: + g = get_globals() + self._device = get_robot_device(g.robot, device_name, WebotsDevice.DistanceSensor) + self._device.enable(g.timestep) + self._mode = GPIOPinMode.INPUT + + def get_mode(self) -> GPIOPinMode: + """Get the current mode of the pin.""" + return self._mode + + def set_mode(self, mode: GPIOPinMode) -> None: + """Set the mode of the pin.""" + self._mode = mode + + def get_digital(self) -> bool: + """Get the digital input value of the pin. This is always False.""" + return False + + def set_digital(self, value: bool) -> None: + """ + Set the digital output value of the pin. + + This has no effect here. + """ + pass + + def get_analog(self) -> int: + """Get the analog input value of the pin. This is always 0.""" + return 0 + + def get_distance(self) -> int: + """ + Get the distance measured by the sensor in mm. + + Relies on the lookup table mapping to the distance in mm. + """ + return int(self._device.getValue()) + + +class MicroSwitch(BasePin): + """A simple switch that can be pressed or released.""" + + def __init__(self, device_name: str) -> None: + g = get_globals() + self._device = get_robot_device(g.robot, device_name, WebotsDevice.TouchSensor) + self._device.enable(g.timestep) + self._mode = GPIOPinMode.INPUT + + def get_mode(self) -> GPIOPinMode: + """Get the current mode of the pin.""" + return self._mode + + def set_mode(self, mode: GPIOPinMode) -> None: + """Set the mode of the pin.""" + self._mode = mode + + def get_digital(self) -> bool: + """Get the digital input value of the pin.""" + return bool(self._device.getValue()) + + def set_digital(self, value: bool) -> None: + """ + Set the digital output value of the pin. + + This has no effect here. + """ + pass + + def get_analog(self) -> int: + """Get the analog input value of the pin, either 0 or 1023.""" + return 1023 if self.get_digital() else 0 + + +class PressureSensor(BasePin): + """ + A sensor that can measure the force applied to it. + + This is attached to the pin specified, with the force proportional to the analog value. + """ + + # Use lookupTable [0 0 0, 50 1023 0] // 50 Newton max force + def __init__(self, device_name: str) -> None: + g = get_globals() + self._device = get_robot_device(g.robot, device_name, WebotsDevice.TouchSensor) + self._device.enable(g.timestep) + self._mode = GPIOPinMode.INPUT + + def get_mode(self) -> GPIOPinMode: + """Get the current mode of the pin.""" + return self._mode + + def set_mode(self, mode: GPIOPinMode) -> None: + """Set the mode of the pin.""" + self._mode = mode + + def get_digital(self) -> bool: + """ + Get the digital input value of the pin. + + True when the force is above half the maximum value. + """ + return self.get_analog() > ANALOG_MAX / 2 + + def set_digital(self, value: bool) -> None: + """ + Set the digital output value of the pin. + + This has no effect here. + """ + pass + + def get_analog(self) -> int: + """Get the analog input value of the pin. This is proportional to the force applied.""" + return int(self._device.getValue()) + + +class ReflectanceSensor(BasePin): + """ + A simple sensor that can detect the reflectance of a surface. + + Used for line following, with a higher value indicating a lighter surface. + """ + + def __init__(self, device_name: str) -> None: + g = get_globals() + self._device = get_robot_device(g.robot, device_name, WebotsDevice.DistanceSensor) + self._device.enable(g.timestep) + self._mode = GPIOPinMode.INPUT + + def get_mode(self) -> GPIOPinMode: + """Get the current mode of the pin.""" + return self._mode + + def set_mode(self, mode: GPIOPinMode) -> None: + """Set the mode of the pin.""" + self._mode = mode + + def get_digital(self) -> bool: + """ + Get the digital input value of the pin. + + True when the reflectance is above half the maximum value. + """ + return self.get_analog() > ANALOG_MAX / 2 + + def set_digital(self, value: bool) -> None: + """ + Set the digital output value of the pin. + + This has no effect here. + """ + pass + + def get_analog(self) -> int: + """ + Get the analog input value of the pin. + + This is proportional to the reflectance of the surface. + """ + return int(self._device.getValue()) + + +class Led(BasePin): + """A simple LED that can be turned on or off.""" + + def __init__(self, device_name: str) -> None: + self._led = _Led(device_name) + self._mode = GPIOPinMode.OUTPUT + + def get_mode(self) -> GPIOPinMode: + """Get the current mode of the pin.""" + return self._mode + + def set_mode(self, mode: GPIOPinMode) -> None: + """Set the mode of the pin.""" + self._mode = mode + + def get_digital(self) -> bool: + """ + Get the digital input value of the pin. + + True when the LED is on. + """ + return self._led.get_colour() > 0 + + def set_digital(self, value: bool) -> None: + """Set the digital output value of the pin. This turns the LED on or off.""" + self._led.set_colour(1 if value else 0) + + def get_analog(self) -> int: + """Get the analog input value of the pin. This is always 0.""" + return 0 diff --git a/simulator/modules/sbot_interface/devices/camera.py b/simulator/modules/sbot_interface/devices/camera.py new file mode 100644 index 0000000..d8a271b --- /dev/null +++ b/simulator/modules/sbot_interface/devices/camera.py @@ -0,0 +1,105 @@ +"""A wrapper for the Webots camera device.""" +from __future__ import annotations + +from abc import ABC, abstractmethod +from functools import lru_cache +from math import tan + +from sbot_interface.devices.util import WebotsDevice, get_globals, get_robot_device + +g = get_globals() + + +class BaseCamera(ABC): + """Base class for camera devices.""" + + @abstractmethod + def get_image(self) -> bytes: + """Get a frame from the camera, encoded as a byte string.""" + pass + + @abstractmethod + def get_resolution(self) -> tuple[int, int]: + """Get the resolution of the camera in pixels, width x height.""" + pass + + @abstractmethod + def get_calibration(self) -> tuple[float, float, float, float]: + """Return the intrinsic camera calibration parameters fx, fy, cx, cy.""" + pass + + +class NullCamera(BaseCamera): + """ + Null camera device. + + Allows the robot to run without a camera device attached. + """ + + def get_image(self) -> bytes: + """Get a frame from the camera, encoded as a byte string.""" + return b'' + + def get_resolution(self) -> tuple[int, int]: + """Get the resolution of the camera in pixels, width x height.""" + return 0, 0 + + def get_calibration(self) -> tuple[float, float, float, float]: + """Return the intrinsic camera calibration parameters fx, fy, cx, cy.""" + return 0, 0, 0, 0 + + +# Camera +class Camera(BaseCamera): + """ + A wrapper for the Webots camera device. + + The camera will sleep for 1 frame time before capturing an image to ensure the + image is up to date. + + :param device_name: The name of the camera device. + :param frame_rate: The frame rate of the camera in frames per second. + """ + + def __init__(self, device_name: str, frame_rate: int) -> None: + self._device = get_robot_device(g.robot, device_name, WebotsDevice.Camera) + # round down to the nearest timestep + self.sample_time = int(((1000 / frame_rate) // g.timestep) * g.timestep) + + def get_image(self) -> bytes: + """ + Get a frame from the camera, encoded as a byte string. + + Sleeps for 1 frame time before capturing the image to ensure the image is up to date. + + NOTE The image data buffer is automatically freed at the end of the timestep, + so must not be accessed after any sleep. + + :return: The image data as a byte string. + """ + # A frame is only captured every sample_time milliseconds the camera is enabled + # so we need to wait for a frame to be captured after enabling the camera. + # The image data buffer is automatically freed at the end of the timestep. + self._device.enable(self.sample_time) + g.sleep(self.sample_time / 1000) + + image_data_raw = self._device.getImage() + # Disable the camera to save computation + self._device.disable() # type: ignore[no-untyped-call] + + return image_data_raw + + @lru_cache + def get_resolution(self) -> tuple[int, int]: + """Get the resolution of the camera in pixels, width x height.""" + return self._device.getWidth(), self._device.getHeight() + + @lru_cache + def get_calibration(self) -> tuple[float, float, float, float]: + """Return the intrinsic camera calibration parameters fx, fy, cx, cy.""" + return ( + (self._device.getWidth() / 2) / tan(self._device.getFov() / 2), # fx + (self._device.getWidth() / 2) / tan(self._device.getFov() / 2), # fy + self._device.getWidth() // 2, # cx + self._device.getHeight() // 2, # cy + ) diff --git a/simulator/modules/sbot_interface/devices/led.py b/simulator/modules/sbot_interface/devices/led.py new file mode 100644 index 0000000..8a61a7d --- /dev/null +++ b/simulator/modules/sbot_interface/devices/led.py @@ -0,0 +1,88 @@ +""" +A wrapper for the Webots LED device. + +Supports both single and multi-colour non-PWM LEDs. +""" +from abc import ABC, abstractmethod + +from sbot_interface.devices.util import WebotsDevice, get_globals, get_robot_device + +RGB_COLOURS = [ + (False, False, False), # OFF + (True, False, False), # RED + (True, True, False), # YELLOW + (False, True, False), # GREEN + (False, True, True), # CYAN + (False, False, True), # BLUE + (True, False, True), # MAGENTA + (True, True, True), # WHITE +] + + +class BaseLed(ABC): + """Base class for LED devices.""" + + def __init__(self) -> None: + self.colour = 0 + + @abstractmethod + def set_colour(self, colour: int) -> None: + """Set the colour of the LED.""" + pass + + @abstractmethod + def get_colour(self) -> int: + """Get the colour of the LED.""" + pass + + +class NullLed(BaseLed): + """Null LED device. Allows the robot to run without an LED device attached.""" + + def __init__(self) -> None: + self.colour = 0 + + def set_colour(self, colour: int) -> None: + """Set the colour of the LED.""" + self.colour = colour + + def get_colour(self) -> int: + """Get the colour of the LED.""" + return self.colour + + +class Led(BaseLed): + """ + A wrapper for the Webots LED device. + + :param device_name: The name of the LED device. + :param num_colours: The number of colours the LED supports. + """ + + def __init__(self, device_name: str, num_colours: int = 1) -> None: + g = get_globals() + self.num_colours = num_colours + self._device = get_robot_device(g.robot, device_name, WebotsDevice.LED) + + def set_colour(self, colour: int) -> None: + """ + Set the colour of the LED. + + :param colour: The colour to set the LED to. A 1-based index for the lookup + table of the LED. 0 is OFF. + """ + if 0 <= colour < self.num_colours: + # NOTE: value 0 is OFF + self._device.set(colour) + else: + raise ValueError(f'Invalid colour: {colour}') + + def get_colour(self) -> int: + """ + Get the colour of the LED. + + :return: The colour of the LED. A 1-based index for the lookup table of the LED. + 0 is OFF. + """ + # webots uses 1-based indexing + return self._device.get() diff --git a/simulator/modules/sbot_interface/devices/motor.py b/simulator/modules/sbot_interface/devices/motor.py new file mode 100644 index 0000000..7f11d87 --- /dev/null +++ b/simulator/modules/sbot_interface/devices/motor.py @@ -0,0 +1,155 @@ +""" +A wrapper for the Webots motor device. + +The motor will apply a small amount of variation to the power setting to simulate +inaccuracies in the motor. +""" +import logging +from abc import ABC, abstractmethod + +from sbot_interface.devices.util import ( + WebotsDevice, + add_jitter, + get_globals, + get_robot_device, + map_to_range, +) + +MAX_POWER = 1000 +MIN_POWER = -1000 + + +class BaseMotor(ABC): + """Base class for motor devices.""" + + @abstractmethod + def disable(self) -> None: + """Disable the motor.""" + pass + + @abstractmethod + def set_power(self, value: int) -> None: + """Set the power of the motor (±1000).""" + pass + + @abstractmethod + def get_power(self) -> int: + """Get the power of the motor.""" + pass + + @abstractmethod + def get_current(self) -> int: + """Get the current draw of the motor in mA.""" + pass + + @abstractmethod + def enabled(self) -> bool: + """Check if the motor is enabled.""" + pass + + +class NullMotor(BaseMotor): + """Null motor device. Allows the robot to run without a motor device attached.""" + + def __init__(self) -> None: + self.power = 0 + self._enabled = False + + def disable(self) -> None: + """Disable the motor.""" + self._enabled = False + + def set_power(self, value: int) -> None: + """Set the power of the motor.""" + self.power = value + self._enabled = True + + def get_power(self) -> int: + """Get the power of the motor.""" + return self.power + + def get_current(self) -> int: + """Get the current draw of the motor in mA.""" + return 0 + + def enabled(self) -> bool: + """Check if the motor is enabled.""" + return self._enabled + + +class Motor(BaseMotor): + """ + A wrapper for the Webots motor device. + + The motor will apply a small amount of variation to the power setting to simulate + inaccuracies in the motor. + + :param device_name: The name of the motor device. + """ + + def __init__(self, device_name: str) -> None: + self.power = 0 + self._enabled = False + g = get_globals() + self._device = get_robot_device(g.robot, device_name, WebotsDevice.Motor) + # Put the motor in velocity control mode + self._device.setPosition(float('inf')) + self._device.setVelocity(0) + self._max_speed = self._device.getMaxVelocity() + # Limit the torque the motor can apply to have realistic acceleration + self._device.setAvailableTorque(1) + + def disable(self) -> None: + """Disable the motor.""" + self._device.setVelocity(0) + self._enabled = False + + def set_power(self, value: int) -> None: + """ + Set the power of the motor. + + :param value: The power setting for the motor. A value between -1000 and 1000. + """ + if value != 0: + if abs(value) < 0.05: + logging.warning( + "Motor power is too low, values below 0.05 will not move the motor." + ) + value = 0 + else: + # Apply a small amount of variation to the power setting to simulate + # inaccuracies in the motor + value = int(add_jitter(value, (MIN_POWER, MAX_POWER))) + + self._device.setVelocity(map_to_range( + value, + (MIN_POWER, MAX_POWER), + (-self._max_speed, self._max_speed), + )) + self.power = value + self._enabled = True + + def get_power(self) -> int: + """ + Get the power of the motor. + + :return: The power setting for the motor. A value between -1000 and 1000. + """ + return self.power + + def get_current(self) -> int: + """ + Get the current draw of the motor in mA. + + :return: The current draw of the motor in mA. + """ + # TODO calculate from torque feedback + return 0 + + def enabled(self) -> bool: + """ + Check if the motor is enabled. + + :return: True if the motor is enabled, False otherwise. + """ + return self._enabled diff --git a/simulator/modules/sbot_interface/devices/power.py b/simulator/modules/sbot_interface/devices/power.py new file mode 100644 index 0000000..71b06d1 --- /dev/null +++ b/simulator/modules/sbot_interface/devices/power.py @@ -0,0 +1,137 @@ +"""A module to define the power devices used in the simulator.""" +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Callable + +from sbot_interface.devices.util import WebotsDevice, get_globals, get_robot_device + + +class Output: + """ + A class to represent a power output. + + This does not actually represent any device in the simulator, + but is used to simulate how the outputs on the power board would behave. + + :param downstream_current: A function to get the current draw of the downstream device. + """ + + def __init__(self, downstream_current: Callable[[], int] | None = None) -> None: + self._enabled = False + self._current_func = downstream_current + + def set_output(self, enable: bool) -> None: + """Set the output state.""" + self._enabled = enable + + def get_output(self) -> bool: + """Get the output state.""" + return self._enabled + + def get_current(self) -> int: + """Get the current draw of the output in mA.""" + if self._current_func is not None: + return self._current_func() + return 0 + + +class ConnectorOutput(Output): + """ + A class to represent a power output that controls a webots connector device. + + :param device_name: The name of the device in webots. + :param downstream_current: A function to get the current draw of the downstream device. + """ + + def __init__( + self, + device_name: str, + downstream_current: Callable[[], int] | None = None, + ) -> None: + super().__init__(downstream_current) + g = get_globals() + self._device = get_robot_device(g.robot, device_name, WebotsDevice.Connector) + self._enabled = False + + def set_output(self, enable: bool) -> None: + """Set the output state.""" + if enable: + self._device.lock() # type: ignore[no-untyped-call] + else: + self._device.unlock() # type: ignore[no-untyped-call] + + def get_output(self) -> bool: + """Get the output state.""" + return self._device.isLocked() + + +class BaseBuzzer(ABC): + """The base class for the buzzer device.""" + + @abstractmethod + def set_note(self, freq: int, dur: int) -> None: + """Set the note to play and its duration.""" + pass + + @abstractmethod + def get_note(self) -> tuple[int, int]: + """Get the note that is currently playing and its duration.""" + pass + + +class BaseButton(ABC): + """The base class for the button device.""" + + @abstractmethod + def get_state(self) -> bool: + """Get whether the button is pressed.""" + pass + + +class NullBuzzer(BaseBuzzer): + """A buzzer that does nothing. Used for buzzers that are not connected.""" + + def __init__(self) -> None: + self.frequency = 0 + self.duration = 0 + super().__init__() + + def set_note(self, freq: int, dur: int) -> None: + """Set the note to play.""" + self.frequency = freq + self.duration = dur + + def get_note(self) -> tuple[int, int]: + """Get the note that is currently playing and its duration.""" + return self.frequency, self.duration + + +class NullButton(BaseButton): + """A button that does nothing. Used for buttons that are not connected.""" + + def get_state(self) -> bool: + """Return whether the button is pressed. Always returns True.""" + # button is always pressed + return True + + +class StartButton(BaseButton): + """ + A button to represent the start button on the robot. + + Uses the robot's custom data to determine if the robot is ready to start. + """ + + def __init__(self) -> None: + self._initialized = False + + def get_state(self) -> bool: + """Return whether the start button is pressed.""" + g = get_globals() + if not self._initialized: + if g.robot.getCustomData() != 'start': + g.robot.setCustomData('ready') + self._initialized = True + + return bool(g.robot.getCustomData() == 'start') diff --git a/simulator/modules/sbot_interface/devices/servo.py b/simulator/modules/sbot_interface/devices/servo.py new file mode 100644 index 0000000..a71f040 --- /dev/null +++ b/simulator/modules/sbot_interface/devices/servo.py @@ -0,0 +1,157 @@ +""" +A wrapper for the Webots servo device. + +The servo will apply a small amount of variation to the power setting to simulate +inaccuracies in the servo. +""" +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING + +from sbot_interface.devices.util import ( + WebotsDevice, + add_jitter, + get_globals, + get_robot_device, + map_to_range, +) + +if TYPE_CHECKING: + from controller import PositionSensor + +MAX_POSITION = 4000 +MIN_POSITION = 500 +SERVO_MAX = 2000 +SERVO_MIN = 1000 + + +class BaseServo(ABC): + """The base class for all the servos that can be connected to the Servo board.""" + + @abstractmethod + def disable(self) -> None: + """Disable the servo.""" + pass + + @abstractmethod + def set_position(self, value: int) -> None: + """ + Set the position of the servo. + + Position is the pulse width in microseconds. + """ + pass + + @abstractmethod + def get_position(self) -> int: + """Return the current position of the servo.""" + pass + + @abstractmethod + def get_current(self) -> int: + """Return the current draw of the servo in mA.""" + pass + + @abstractmethod + def enabled(self) -> bool: + """Return whether the servo is enabled.""" + pass + + +class NullServo(BaseServo): + """A servo that does nothing. Used for servos that are not connected.""" + + def __init__(self) -> None: + self.position = 1500 + self._enabled = False + + def disable(self) -> None: + """Disable the servo.""" + self._enabled = False + + def set_position(self, value: int) -> None: + """ + Set the position of the servo. + + Position is the pulse width in microseconds. + """ + self.position = value + self._enabled = True + + def get_position(self) -> int: + """ + Return the current position of the servo. + + Position is the pulse width in microseconds. + """ + return self.position + + def get_current(self) -> int: + """Return the current draw of the servo in mA.""" + return 0 + + def enabled(self) -> bool: + """Return whether the servo is enabled.""" + return self._enabled + + +class Servo(BaseServo): + """A servo connected to the Servo board.""" + + def __init__(self, device_name: str) -> None: + self.position = (SERVO_MAX + SERVO_MIN) // 2 + # TODO use setAvailableForce to simulate disabled + self._enabled = False + g = get_globals() + self._device = get_robot_device(g.robot, device_name, WebotsDevice.Motor) + self._pos_sensor: PositionSensor | None = self._device.getPositionSensor() # type: ignore[no-untyped-call] + self._max_position = self._device.getMaxPosition() + self._min_position = self._device.getMinPosition() + if self._pos_sensor is not None: + self._pos_sensor.enable(g.timestep) + + def disable(self) -> None: + """Disable the servo.""" + self._enabled = False + + def set_position(self, value: int) -> None: + """ + Set the position of the servo. + + Position is the pulse width in microseconds. + """ + # Apply a small amount of variation to the power setting to simulate + # inaccuracies in the servo + value = int(add_jitter(value, (SERVO_MIN, SERVO_MAX), std_dev_percent=0.5)) + + self._device.setPosition(map_to_range( + value, + (SERVO_MIN, SERVO_MAX), + (self._min_position + 0.001, self._max_position - 0.001), + )) + self.position = value + self._enabled = True + + def get_position(self) -> int: + """ + Return the current position of the servo. + + Position is the pulse width in microseconds. + """ + if self._pos_sensor is not None: + self.position = int(map_to_range( + self._pos_sensor.getValue(), + (self._min_position + 0.001, self._max_position - 0.001), + (MIN_POSITION, MAX_POSITION), + )) + return self.position + + def get_current(self) -> int: + """Return the current draw of the servo in mA.""" + # TODO calculate from torque feedback + return 0 + + def enabled(self) -> bool: + """Return whether the servo is enabled.""" + return self._enabled diff --git a/simulator/modules/sbot_interface/devices/util.py b/simulator/modules/sbot_interface/devices/util.py new file mode 100644 index 0000000..a160215 --- /dev/null +++ b/simulator/modules/sbot_interface/devices/util.py @@ -0,0 +1,159 @@ +"""Utility functions for the devices module.""" +from __future__ import annotations + +import threading +from dataclasses import dataclass +from math import ceil +from random import gauss +from typing import TypeVar + +from controller import ( + GPS, + LED, + Accelerometer, + Camera, + Compass, + Connector, + DistanceSensor, + Emitter, + Gyro, + InertialUnit, + Lidar, + LightSensor, + Motor, + PositionSensor, + Radar, + RangeFinder, + Receiver, + Robot, + Speaker, + TouchSensor, + VacuumGripper, +) +from controller.device import Device + +TDevice = TypeVar('TDevice', bound=Device) +__GLOBALS: 'GlobalData' | None = None + + +class WebotsDevice: + """ + A collection of Webots device classes. + + Each class represents a different device that can be attached to the robot. + """ + + Accelerometer = Accelerometer + Camera = Camera + Compass = Compass + Connector = Connector + DistanceSensor = DistanceSensor + Emitter = Emitter + GPS = GPS + Gyro = Gyro + InertialUnit = InertialUnit + LED = LED + Lidar = Lidar + LightSensor = LightSensor + Motor = Motor + PositionSensor = PositionSensor + Radar = Radar + RangeFinder = RangeFinder + Receiver = Receiver + Speaker = Speaker + TouchSensor = TouchSensor + VacuumGripper = VacuumGripper + + +@dataclass +class GlobalData: + """ + Global data and functions for the simulator. + + When accessed through the get_globals function, a single instance of this + class is created and stored in the module's global scope. + + :param robot: The robot object. + :param timestep: The timestep size of the simulation. + :param stop_event: The event to stop the simulation. + """ + + robot: Robot + timestep: int + stop_event: threading.Event | None = None + + def sleep(self, secs: float) -> None: + """Sleeps for a given duration in simulator time.""" + if secs == 0: + return + elif secs < 0: + raise ValueError("Sleep duration must be non-negative.") + + # Convert to a multiple of the timestep + msecs = ceil((secs * 1000) / self.timestep) * self.timestep + + # Sleep for the given duration + result = self.robot.step(msecs) + + # If the simulation has stopped, set the stop event + if (result == -1) and (self.stop_event is not None): + self.stop_event.set() + + +def get_globals() -> GlobalData: + """Returns the global dictionary.""" + global __GLOBALS + if __GLOBALS is None: + # Robot constructor lacks a return type annotation in R2023b + robot = Robot() if Robot.created is None else Robot.created # type: ignore[no-untyped-call] + + __GLOBALS = GlobalData( + robot=robot, + timestep=int(robot.getBasicTimeStep()), + ) + return __GLOBALS + + +def map_to_range( + value: float, + old_min_max: tuple[float, float], + new_min_max: tuple[float, float], +) -> float: + """Maps a value from within one range of inputs to within a range of outputs.""" + old_min, old_max = old_min_max + new_min, new_max = new_min_max + return ((value - old_min) / (old_max - old_min)) * (new_max - new_min) + new_min + + +def get_robot_device(robot: Robot, name: str, kind: type[TDevice]) -> TDevice: + """ + A helper function to get a device from the robot. + + Raises a TypeError if the device is not found or is not of the correct type. + Weboots normally just returns None if the device is not found. + + :param robot: The robot object. + :param name: The name of the device. + :param kind: The type of the device. + :return: The device object. + :raises TypeError: If the device is not found or is not of the correct type. + """ + device = robot.getDevice(name) + if not isinstance(device, kind): + raise TypeError(f"Failed to get device: {name}.") + return device + + +def add_jitter( + value: float, + value_range: tuple[float, float], + std_dev_percent: float = 2.0, + offset_percent: float = 0.0, +) -> float: + """Adds normally distributed jitter to a given value.""" + std_dev = value * (std_dev_percent / 100.0) + mean_offset = value * (offset_percent / 100.0) + + error = gauss(mean_offset, std_dev) + # Ensure the error is within the range + return max(value_range[0], min(value_range[1], value + error)) diff --git a/simulator/modules/sbot_interface/setup.py b/simulator/modules/sbot_interface/setup.py new file mode 100644 index 0000000..5f022b0 --- /dev/null +++ b/simulator/modules/sbot_interface/setup.py @@ -0,0 +1,134 @@ +""" +Setup the devices connected to the robot. + +The main configuration for the devices connected to the robot is the devices +list in the setup_devices function. +""" +from __future__ import annotations + +import logging + +from sbot_interface.boards import ( + Arduino, + CameraBoard, + LedBoard, + MotorBoard, + PowerBoard, + ServoBoard, + TimeServer, +) +from sbot_interface.devices.arduino_devices import ( + EmptyPin, + MicroSwitch, + ReflectanceSensor, + UltrasonicSensor, +) +from sbot_interface.devices.camera import Camera +from sbot_interface.devices.led import Led, NullLed +from sbot_interface.devices.motor import Motor +from sbot_interface.devices.power import NullBuzzer, Output, StartButton +from sbot_interface.devices.servo import NullServo +from sbot_interface.socket_server import Board, DeviceServer, SocketServer + + +def setup_devices(log_level: int | str = logging.WARNING) -> SocketServer: + """ + Setup the devices connected to the robot. + + Contains the main configuration for the devices connected to the robot. + + :param log_level: The logging level to use for the device logger. + :return: The socket server which will handle all connections and commands. + """ + device_logger = logging.getLogger('sbot_interface') + device_logger.setLevel(log_level) + + # this is the configuration of devices connected to the robot + devices: list[Board] = [ + PowerBoard( + outputs=[Output() for _ in range(7)], + buzzer=NullBuzzer(), + button=StartButton(), + leds=(NullLed(), NullLed()), + asset_tag='PWR', + ), + MotorBoard( + motors=[ + Motor('left motor'), + Motor('right motor'), + ], + asset_tag='MOT', + ), + ServoBoard( + servos=[NullServo() for _ in range(8)], + asset_tag='SERVO', + ), + LedBoard( + leds=[ + Led('led 1', num_colours=8), + Led('led 2', num_colours=8), + Led('led 3', num_colours=8), + ], + asset_tag='LED', + ), + Arduino( + pins=[ + EmptyPin(), # pin 0 + EmptyPin(), # pin 1 + EmptyPin(), # ultrasonic trigger pin, pin 2 + UltrasonicSensor('ultrasound front'), # pin 3 + EmptyPin(), # ultrasonic trigger pin, pin 4 + UltrasonicSensor('ultrasound left'), # pin 5 + EmptyPin(), # ultrasonic trigger pin, pin 6 + UltrasonicSensor('ultrasound right'), # pin 7 + EmptyPin(), # ultrasonic trigger pin, pin 8 + UltrasonicSensor('ultrasound back'), # pin 9 + MicroSwitch('front left bump sensor'), # pin 10 + MicroSwitch('front right bump sensor'), # pin 11 + MicroSwitch('rear left bump sensor'), # pin 12 + MicroSwitch('rear right bump sensor'), # pin 13 + ReflectanceSensor('left reflectance sensor'), # pin A0 + ReflectanceSensor('center reflectance sensor'), # pin A1 + ReflectanceSensor('right reflectance sensor'), # pin A2 + EmptyPin(), # pin A3 + EmptyPin(), # pin A4 + EmptyPin(), # pin A5 + ], + asset_tag='Arduino1', + ), + TimeServer( + asset_tag='TimeServer', + ), + CameraBoard( + Camera('camera', frame_rate=15), + asset_tag='Camera', + ), + ] + + device_servers: list[DeviceServer] = [] + + for device in devices: + # connect each device to a socket to receive commands from sr-robot3 + device_servers.append(DeviceServer(device)) + + # collect all device servers into a single server which will handle all connections + # and commands + return SocketServer(device_servers) + + +def main() -> None: + """ + Main function to setup and run the devices. Only used for testing. + + This function will setup the devices and start the select loop to handle all connections. + """ + server = setup_devices(logging.DEBUG) + # generate and print the socket url and information for each device + print(server.links_formatted()) + # start select loop for all server sockets and device sockets + server.run() + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + main() diff --git a/simulator/modules/sbot_interface/socket_server.py b/simulator/modules/sbot_interface/socket_server.py new file mode 100644 index 0000000..c820de9 --- /dev/null +++ b/simulator/modules/sbot_interface/socket_server.py @@ -0,0 +1,228 @@ +"""A server for multiple devices that can be connected to the simulator.""" +from __future__ import annotations + +import logging +import os +import select +import signal +import socket +import sys +from threading import Event +from typing import Protocol + +from sbot_interface.devices.util import get_globals + +LOGGER = logging.getLogger(__name__) +g = get_globals() + + +class Board(Protocol): + """The interface for all board simulators that can be connected to the simulator.""" + + asset_tag: str + software_version: str + + def handle_command(self, command: str) -> str | bytes: + """ + Process a command string and return the response. + + Bytes type are treated as tag-length-value (TLV) encoded data. + """ + pass + + +class DeviceServer: + """ + A server for a single device that can be connected to the simulator. + + The process_data method is called when data is received from the socket. + Line-delimited commands are processed and responses are sent back. + """ + + def __init__(self, board: Board) -> None: + self.board = board + # create TCP socket server + self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.server_socket.bind(('127.0.0.1', 0)) + self.server_socket.listen(1) # only allow one connection per device + self.server_socket.setblocking(True) + LOGGER.info( + f'Started server for {self.board_type} ({self.board.asset_tag}) ' + f'on port {self.port}' + ) + + self.device_socket: socket.socket | None = None + self.buffer = b'' + + def process_data(self, data: bytes) -> bytes | None: + """Process incoming data if a line has been received and return the response.""" + self.buffer += data + if b'\n' in self.buffer: + # Sleep to simulate processing time + g.sleep(g.timestep / 1000) + data, self.buffer = self.buffer.split(b'\n', 1) + return self.run_command(data.decode().strip()) + else: + return None + + def run_command(self, command: str) -> bytes: + """ + Process a command and return the response. + + Wraps the board's handle_command method and deals with exceptions and data types. + """ + LOGGER.debug(f'> {command}') + try: + response = self.board.handle_command(command) + if isinstance(response, bytes): + LOGGER.debug(f'< {len(response)} bytes') + return response + else: + LOGGER.debug(f'< {response}') + return response.encode() + b'\n' + except Exception as e: + LOGGER.exception(f'Error processing command: {command}') + return f'NACK:{e}\n'.encode() + + def flush_buffer(self) -> None: + """Clear the internal buffer of received data.""" + self.buffer = b'' + + def socket(self) -> socket.socket: + """ + Return the socket to select on. + + If the device is connected, return the device socket. + Otherwise, return the server socket. + """ + if self.device_socket is not None: + # ignore the server socket while we are connected + return self.device_socket + else: + return self.server_socket + + def accept(self) -> None: + """Accept a connection from a device and set the device socket to blocking.""" + if self.device_socket is not None: + self.disconnect_device() + self.device_socket, _ = self.server_socket.accept() + self.device_socket.setblocking(True) + LOGGER.info(f'Connected to {self.asset_tag} from {self.device_socket.getpeername()}') + + def disconnect_device(self) -> None: + """Close the device socket, flushing the buffer first.""" + self.flush_buffer() + if self.device_socket is not None: + self.device_socket.close() + self.device_socket = None + LOGGER.info(f'Disconnected from {self.asset_tag}') + + def close(self) -> None: + """Close the server and client sockets.""" + self.disconnect_device() + self.server_socket.close() + + def __del__(self) -> None: + self.close() + + @property + def port(self) -> int: + """Return the port number of the server socket.""" + if self.server_socket is None: + return -1 + return int(self.server_socket.getsockname()[1]) + + @property + def asset_tag(self) -> str: + """Return the asset tag of the board.""" + return self.board.asset_tag + + @property + def board_type(self) -> str: + """Return the class name of the board object.""" + return self.board.__class__.__name__ + + +class SocketServer: + """ + A server for multiple devices that can be connected to the simulator. + + The run method blocks until the stop_event is set. + """ + + def __init__(self, devices: list[DeviceServer]) -> None: + self.devices = devices + self.stop_event = Event() + g.stop_event = self.stop_event + # flag to indicate that we are exiting because the usercode has completed + self.completed = False + + def run(self) -> None: + """ + Run the server, accepting connections and processing data. + + This method blocks until the stop_event is set. + """ + while not self.stop_event.is_set(): + # select on all server sockets and device sockets + sockets = [device.socket() for device in self.devices] + + readable, _, _ = select.select(sockets, [], [], 0.5) + + for device in self.devices: + try: + if device.server_socket in readable: + device.accept() + + if device.device_socket in readable and device.device_socket is not None: + try: + if sys.platform == 'win32': + data = device.device_socket.recv(4096) + else: + data = device.device_socket.recv(4096, socket.MSG_DONTWAIT) + except ConnectionError: + device.disconnect_device() + continue + + if not data: + device.disconnect_device() + else: + response = device.process_data(data) + if response is not None: + try: + device.device_socket.sendall(response) + except ConnectionError: + device.disconnect_device() + continue + except Exception as e: + LOGGER.exception(f"Failure in simulated boards: {e}") + + LOGGER.info('Stopping server') + for device in self.devices: + device.close() + + if self.stop_event.is_set() and self.completed is False: + # Stop the usercode + os.kill(os.getpid(), signal.SIGINT) + + def links(self) -> dict[str, dict[str, str]]: + """Return a mapping of asset tags to ports, grouped by board type.""" + return { + device.asset_tag: { + 'board_type': device.board_type, + 'port': str(device.port), + } + for device in self.devices + } + + def links_formatted(self, address: str = '127.0.0.1') -> str: + """ + Return a formatted string of all the links to the devices. + + The format is 'socket://address:port/board_type/asset_tag'. + Each link is separated by a newline. + """ + return '\n'.join( + f"socket://{address}:{data['port']}/{data['board_type']}/{asset_tag}" + for asset_tag, data in self.links().items() + ) diff --git a/simulator/protos/SR2025bot.proto b/simulator/protos/SR2025bot.proto new file mode 100755 index 0000000..c05edcc --- /dev/null +++ b/simulator/protos/SR2025bot.proto @@ -0,0 +1,204 @@ +#VRML_SIM R2023b utf8 +EXTERNPROTO "./robot/MotorAssembly.proto" +EXTERNPROTO "./robot/Caster.proto" +EXTERNPROTO "./robot/RobotCamera.proto" +EXTERNPROTO "./robot/UltrasoundModule.proto" +EXTERNPROTO "./robot/RGBLed.proto" +EXTERNPROTO "./robot/ReflectanceSensor.proto" +EXTERNPROTO "./robot/Flag.proto" +EXTERNPROTO "./robot/BumpSensor.proto" +EXTERNPROTO "./robot/VacuumSucker.proto" + +PROTO SR2025bot [ + field SFString name "" + field SFVec3f translation 0 0 0 + field SFRotation rotation 0 0 1 0 + field SFString controller "" + field MFString controllerArgs [] + field SFString customData "" + field SFColor flagColour 1 1 1 +] { + Robot { + name IS name + translation IS translation + rotation IS rotation + controller IS controller + controllerArgs IS controllerArgs + customData IS customData + children [ + Pose { + translation 0 0 0.049 + children [ + MotorAssembly { + name "left motor" + rotation 0 0 1 3.1415 + reversed TRUE + translation 0 0.14 0 + } + MotorAssembly { + name "right motor" + translation 0 -0.14 0 + } + Caster { + name "caster" + translation -0.15 0 -0.045 + } + DEF BASE Solid { + translation -0.065 0 -0.02 + children [ + Shape { + appearance PBRAppearance { + baseColor 0.757 0.604 0.424 + roughness 1 + metalness 0 + } + geometry Box { + size 0.25 0.25 0.02 + } + } + ] + name "Chassis" + boundingObject DEF BASE_GEO Box { + size 0.25 0.25 0.02 + } + physics Physics { + density 2000 # 66% Aluminium + } + } + DEF BOARD Solid { + translation -0.05 0 0 + rotation 0 0 1 1.5708 + children [ + Shape { + appearance PBRAppearance { + baseColor 0 0 0 + roughness 1 + metalness 0 + } + geometry Box { + size 0.08 0.06 0.02 + } + } + ] + name "Board" + } + DEF STABILISER Solid { + translation -0.16 0 0 + children [ + Shape { + appearance PBRAppearance { + baseColor 0.8 0.8 0.75 + roughness 1 + metalness 0 + } + geometry DEF WEIGHT_GEO Box { + size 0.04 0.15 0.02 + } + } + ] + name "Chassis weight" + boundingObject USE WEIGHT_GEO + physics Physics { + density 8000 # Steel + } + } + RobotCamera { + name "camera" + translation 0.05 0 0.03 + } + Solid { + translation 0.03 0 0 + children [ + Shape { + appearance PBRAppearance { + baseColor 0.4 0.4 0.4 + metalness 0 + } + geometry Box { + size 0.01 0.01 0.03 + } + } + ] + name "Camera riser" + } + VacuumSucker { + name "vacuum sucker" + translation 0.01 0 -0.02 + } + UltrasoundModule { + name "ultrasound front" + translation 0.04 0 0 + } + UltrasoundModule { + name "ultrasound left" + translation -0.08 0.12 0 + rotation 0 0 1 1.5708 + } + UltrasoundModule { + name "ultrasound back" + translation -0.18 0 0 + rotation 0 0 1 3.1416 + } + UltrasoundModule { + name "ultrasound right" + translation -0.08 -0.12 0 + rotation 0 0 1 -1.5708 + } + RGBLed { + name "led 1" + translation -0.11 0.08 -0.008 + } + RGBLed { + name "led 2" + translation -0.11 0 -0.008 + } + RGBLed { + name "led 3" + translation -0.11 -0.08 -0.008 + } + ReflectanceSensor { + name "left reflectance sensor" + translation 0.03 0.02 -0.03 + rotation 0 0 1 1.5708 + } + ReflectanceSensor { + name "center reflectance sensor" + translation 0.03 0 -0.03 + rotation 0 0 1 1.5708 + } + ReflectanceSensor { + name "right reflectance sensor" + translation 0.03 -0.02 -0.03 + rotation 0 0 1 1.5708 + } + BumpSensor { + name "front left bump sensor" + translation 0.06 0.06 -0.02 + } + BumpSensor { + name "front right bump sensor" + translation 0.06 -0.06 -0.02 + } + BumpSensor { + name "rear left bump sensor" + translation -0.19 0.06 -0.02 + } + BumpSensor { + name "rear right bump sensor" + translation -0.19 -0.06 -0.02 + } + Flag { + name "flag" + translation 0.03 0.07 0.09 + flagColour IS flagColour + } + ] + } + ] + boundingObject Pose { + translation -0.065 0 0.029 + children [USE BASE_GEO] + } + physics Physics {} + } +} diff --git a/simulator/protos/SRObot.proto b/simulator/protos/SRObot.proto new file mode 100755 index 0000000..1970cbc --- /dev/null +++ b/simulator/protos/SRObot.proto @@ -0,0 +1,178 @@ +#VRML_SIM R2023b utf8 +EXTERNPROTO "./robot/MotorAssembly.proto" +EXTERNPROTO "./robot/Caster.proto" +EXTERNPROTO "./robot/RobotCamera.proto" +EXTERNPROTO "./robot/UltrasoundModule.proto" +EXTERNPROTO "./robot/RGBLed.proto" +EXTERNPROTO "./robot/ReflectanceSensor.proto" +EXTERNPROTO "./robot/Flag.proto" +EXTERNPROTO "./robot/BumpSensor.proto" + +PROTO SRObot [ + field SFString name "" + field SFVec3f translation 0 0 0 + field SFRotation rotation 0 0 1 0 + field SFString controller "" + field MFString controllerArgs [] + field SFString customData "" + field SFColor flagColour 1 1 1 +] { + Robot { + name IS name + translation IS translation + rotation IS rotation + controller IS controller + controllerArgs IS controllerArgs + customData IS customData + children [ + Pose { + translation 0 0 0.049 + children [ + MotorAssembly { + name "left motor" + rotation 0 0 1 3.1415 + reversed TRUE + translation 0 0.1 0 + } + MotorAssembly { + name "right motor" + translation 0 -0.1 0 + } + Caster { + name "caster" + translation -0.13 0 -0.045 + } + DEF BASE Solid { + translation -0.045 0 -0.02 + children [ + Shape { + appearance PBRAppearance { + baseColor 0.757 0.604 0.424 + roughness 1 + metalness 0 + } + geometry Box { + size 0.21 0.16 0.02 + } + } + ] + name "Chassis" + boundingObject DEF BASE_GEO Box { + size 0.21 0.16 0.02 + } + physics Physics { + density 3000 # Aluminium + } + } + DEF BOARD Solid { + translation -0.06 0 0 + children [ + Shape { + appearance PBRAppearance { + baseColor 0 0 0 + roughness 1 + metalness 0 + } + geometry Box { + size 0.08 0.06 0.02 + } + } + ] + name "Board" + } + RobotCamera { + name "camera" + translation 0 0 0.03 + } + Solid { + translation -0.01 0 0 + children [ + Shape { + appearance PBRAppearance { + baseColor 0.4 0.4 0.4 + metalness 0 + } + geometry Box { + size 0.01 0.01 0.03 + } + } + ] + name "Camera riser" + } + UltrasoundModule { + name "ultrasound front" + translation 0.04 0 0 + } + UltrasoundModule { + name "ultrasound left" + translation -0.08 0.07 0 + rotation 0 0 1 1.5708 + } + UltrasoundModule { + name "ultrasound back" + translation -0.14 0 0 + rotation 0 0 1 3.1416 + } + UltrasoundModule { + name "ultrasound right" + translation -0.08 -0.07 0 + rotation 0 0 1 -1.5708 + } + RGBLed { + name "led 1" + translation -0.12 0.05 -0.008 + } + RGBLed { + name "led 2" + translation -0.12 0 -0.008 + } + RGBLed { + name "led 3" + translation -0.12 -0.05 -0.008 + } + ReflectanceSensor { + name "left reflectance sensor" + translation 0.03 0.02 -0.03 + rotation 0 0 1 1.5708 + } + ReflectanceSensor { + name "center reflectance sensor" + translation 0.03 0 -0.03 + rotation 0 0 1 1.5708 + } + ReflectanceSensor { + name "right reflectance sensor" + translation 0.03 -0.02 -0.03 + rotation 0 0 1 1.5708 + } + BumpSensor { + name "front left bump sensor" + translation 0.06 0.05 -0.02 + } + BumpSensor { + name "front right bump sensor" + translation 0.06 -0.05 -0.02 + } + BumpSensor { + name "rear left bump sensor" + translation -0.15 0.05 -0.02 + } + BumpSensor { + name "rear right bump sensor" + translation -0.15 -0.05 -0.02 + } + Flag { + name "flag" + translation 0.03 0.05 0.09 + flagColour IS flagColour + } + ] + } + ] + boundingObject Pose { + translation -0.045 0 0.029 + children [USE BASE_GEO] + } + physics Physics {} + } +} diff --git a/simulator/protos/arena/Arena.proto b/simulator/protos/arena/Arena.proto new file mode 100755 index 0000000..0754203 --- /dev/null +++ b/simulator/protos/arena/Arena.proto @@ -0,0 +1,136 @@ +#VRML_SIM R2023b utf8 +# template language: javascript + +PROTO Arena [ + field SFVec2f size 1 1 + field SFColor wallColour 0.095395215 0.22841774 0.8000001 + field MFString floorTexture [] + field SFBool locked FALSE +] { + Group { + children [ + DEF ARENA_WALLS Group { + children [ + Solid { # North Wall + translation 0 %<= -(fields.size.value.y / 2 + 0.075) >% 0.15 + children [ + DEF NORTH_WALL Shape { + appearance DEF WALL_COLOUR PBRAppearance { + baseColor IS wallColour + roughness 1 + metalness 0 + } + geometry Box { + size %<= fields.size.value.x >% 0.15 0.3 + } + } + ] + name "North Wall" + locked IS locked + } + Solid { # East Wall + translation %<= -(fields.size.value.x / 2 + 0.075) >% 0 0.15 + children [ + DEF EAST_WALL Shape { + appearance USE WALL_COLOUR + geometry Box { + size 0.15 %<= fields.size.value.y + 0.3 >% 0.3 + } + } + ] + name "East Wall" + locked IS locked + } + Solid { # West Wall + translation %<= fields.size.value.x / 2 + 0.075 >% 0 0.15 + children [USE EAST_WALL] + name "West Wall" + locked IS locked + } + Solid { # South Wall + translation 0 %<= fields.size.value.y / 2 + 0.075 >% 0.15 + children [USE NORTH_WALL] + name "South Wall" + locked IS locked + } + ] + } + DEF ARENA_BOUNDING Group { + children [ + Solid { # Floor + translation 0 -0.002 0 + rotation 0 0 1 3.1416 + children [ + DEF FLOOR Shape { + appearance Appearance { + material Material { + ambientIntensity 0 + } + texture ImageTexture { + url IS floorTexture + repeatS FALSE + repeatT FALSE + filtering 1 + } + } + geometry Plane { + size IS size + } + } + ] + name "Floor" + boundingObject Plane { + size IS size + } + locked IS locked + } + Solid { # Ceiling + rotation 1 0 0 3.1419 + translation 0 0 2 + boundingObject Plane { + size IS size + } + name "Top boundary" + locked IS locked + } + Solid { # North bound + rotation 1 0 0 -1.5708 + translation 0 %<= -fields.size.value.y / 2 >% 1 + boundingObject Plane { + size %<= fields.size.value.x >% 2 + } + name "North boundary" + locked IS locked + } + Solid { # East bound + rotation 0 -1 0 -1.5708 + translation %<= -fields.size.value.x / 2 >% 0 1 + boundingObject Plane { + size 2 %<= fields.size.value.y >% + } + name "East boundary" + locked IS locked + } + Solid { # South bound + rotation 1 0 0 1.5708 + translation 0 %<= fields.size.value.y / 2 >% 1 + boundingObject Plane { + size %<= fields.size.value.x >% 2 + } + name "South boundary" + locked IS locked + } + Solid { # West bound + rotation 0 -1 0 1.5708 + translation %<= fields.size.value.x / 2 >% 0 1 + boundingObject Plane { + size 2 %<= fields.size.value.y >% + } + name "West boundary" + locked IS locked + } + ] + } + ] + } +} \ No newline at end of file diff --git a/simulator/protos/arena/Deck.proto b/simulator/protos/arena/Deck.proto new file mode 100755 index 0000000..0791644 --- /dev/null +++ b/simulator/protos/arena/Deck.proto @@ -0,0 +1,50 @@ +#VRML_SIM R2023b utf8 +# template language: javascript + +PROTO Deck [ + field SFVec2f size 1 1 + field SFVec3f translation 0 0 0.001 + field SFRotation rotation 1 0 0 0 + field SFFloat height 0.17 + field SFColor sideColour 1 1 1 + field SFColor topColour 0.1 0.1 0.1 + field SFBool locked FALSE + field SFString name "" +] { + Solid { + translation IS translation + rotation IS rotation + children [ + Shape { + appearance PBRAppearance { + baseColor IS sideColour + roughness 1 + metalness 0.5 + } + geometry DEF DECK Box { + size %<= fields.size.value.x >% %<= fields.size.value.y >% %<= fields.height.value >% + } + } + Solid { + translation 0 0 %<= fields.height.value / 2 + 0.002 >% + children [ + Shape { + appearance PBRAppearance { + baseColor IS topColour + roughness 1 + metalness 0 + } + geometry Plane { + size %<= fields.size.value.x >% %<= fields.size.value.y >% + } + castShadows FALSE + } + ] + name "Top of deck" + } + ] + boundingObject USE DECK + name IS name + locked IS locked + } +} \ No newline at end of file diff --git a/simulator/protos/arena/Pillar.proto b/simulator/protos/arena/Pillar.proto new file mode 100755 index 0000000..88388d6 --- /dev/null +++ b/simulator/protos/arena/Pillar.proto @@ -0,0 +1,76 @@ +#VRML_SIM R2023b utf8 +# template language: javascript +# tags: nonDeterministic + +EXTERNPROTO "../props/Marker.proto" + +PROTO Pillar [ + field SFVec3f translation 0 0 0 + field SFRotation rotation 0 1 0 0 + field SFVec3f size 0.13 0.13 0.13 + field SFVec2f {0.08 0.08, 0.15 0.15, 0.2 0.2} marker_size 0.08 0.08 + field SFFloat marker_height 0.065 + field SFColor colour 0.9 0.9 0.9 + field SFString marker "0" + field SFString model "" + field MFString texture_url [] +] +{ + Pose { + translation IS translation + rotation IS rotation + children [ + Solid { + translation 0 0 %<= fields.size.value.z / 2 >% + children [ + Shape { + appearance DEF PILLAR_APPEARANCE PBRAppearance { + baseColor IS colour + metalness 0 + roughness 1 + } + geometry DEF PILLAR_GEOMETRY Box { + size IS size + } + } + Marker { + translation 0 %<= fields.size.value.y / 2 + 0.001 >% %<= fields.marker_height.value - (fields.size.value.z / 2) >% + rotation 1 0 0 -1.5708 + size IS marker_size + name "front" + model IS marker + texture_url IS texture_url + } + Marker { + translation 0 %<= -(fields.size.value.y / 2 + 0.001) >% %<= fields.marker_height.value - (fields.size.value.z / 2) >% + rotation 1 0 0 1.5708 + size IS marker_size + name "back" + model IS marker + texture_url IS texture_url + } + Marker { + translation %<= fields.size.value.x / 2 + 0.001 >% 0 %<= fields.marker_height.value - (fields.size.value.z / 2) >% + rotation 0 1 0 1.5708 + size IS marker_size + name "side-1" + model IS marker + texture_url IS texture_url + } + Marker { + translation %<= -(fields.size.value.x / 2 + 0.001) >% 0 %<= fields.marker_height.value - (fields.size.value.z / 2) >% + rotation 0 1 0 -1.5708 + size IS marker_size + name "side-2" + model IS marker + texture_url IS texture_url + } + ] + name IS model + model IS model + boundingObject USE PILLAR_GEOMETRY + locked TRUE + } + ] + } +} diff --git a/simulator/protos/arena/TriangleDeck.proto b/simulator/protos/arena/TriangleDeck.proto new file mode 100755 index 0000000..8dad0ab --- /dev/null +++ b/simulator/protos/arena/TriangleDeck.proto @@ -0,0 +1,68 @@ +#VRML_SIM R2023b utf8 +# template language: javascript +EXTERNPROTO "https://raw.githubusercontent.com/cyberbotics/webots/R2023b/projects/objects/geometries/protos/Extrusion.proto" + +PROTO TriangleDeck [ + field SFVec2f size 1 1 + field SFVec3f translation 0 0 0.001 + field SFRotation rotation 1 0 0 0 + field SFFloat height 0.17 + field SFColor sideColour 1 1 1 + field SFColor topColour 0.1 0.1 0.1 + field SFBool locked FALSE + field SFString name "" +] { + Pose { + translation IS translation + rotation IS rotation + children [ + Solid { + translation 0 0 %<= -fields.height.value / 2 + 0.002 >% + children [ + Shape { + appearance PBRAppearance { + baseColor IS sideColour + roughness 1 + metalness 0.5 + } + geometry DEF DECK Extrusion { + crossSection [0.5 0.5, 0.5 -0.5, -0.5 0.5, 0.5 0.5] + scale %<= fields.size.value.x >% %<= fields.size.value.y >% + spine [0 0 0, 0 0 %<= fields.height.value >%] + splineSubdivision 1 + } + } + Solid { + translation 0 0 %<= fields.height.value + 0.002 >% + children [ + Shape { + appearance PBRAppearance { + baseColor IS topColour + roughness 1 + metalness 0 + } + geometry IndexedFaceSet { + coord Coordinate { + point [ + %<= -fields.size.value.x / 2 >% %<= -fields.size.value.y / 2 >% 0 + %<= fields.size.value.x / 2 >% %<= -fields.size.value.y / 2 >% 0 + %<= -fields.size.value.x / 2 >% %<= fields.size.value.y / 2 >% 0 + ] + } + coordIndex [ + 0 1 2 0 + ] + } + castShadows FALSE + } + ] + name "Top of deck" + } + ] + boundingObject USE DECK + name IS name + locked IS locked + } + ] + } +} \ No newline at end of file diff --git a/simulator/protos/props/BoxToken.proto b/simulator/protos/props/BoxToken.proto new file mode 100755 index 0000000..461d7fa --- /dev/null +++ b/simulator/protos/props/BoxToken.proto @@ -0,0 +1,125 @@ +#VRML_SIM R2023b utf8 +# template language: javascript +# tags: nonDeterministic + +EXTERNPROTO "./Marker.proto" + +PROTO BoxToken [ + field SFVec3f translation 0 0 0 + field SFRotation rotation 0 1 0 0 + field SFVec3f size 0.13 0.13 0.13 + field SFVec2f {0.08 0.08, 0.15 0.15, 0.2 0.2} marker_size 0.08 0.08 + field SFColor colour 0.7 0.55 0.35 + field SFString marker "0" + field SFString model "" + field SFFloat mass 0.080 + field MFString texture_url [] + field SFFloat connectorStrength 35 + field SFFloat connectorShear 20 +] +{ + Solid { + translation IS translation + rotation IS rotation + children [ + Shape { + appearance DEF TOKEN_APPEARANCE PBRAppearance { + baseColor IS colour + metalness 0 + roughness 1 + } + geometry DEF TOKEN_GEOMETRY Box { + size IS size + } + } + Marker { + translation 0 %<= fields.size.value.y / 2 + 0.001 >% 0 + rotation 1 0 0 -1.5708 + size IS marker_size + name "front" + model IS marker + texture_url IS texture_url + } + Marker { + translation 0 %<= -(fields.size.value.y / 2 + 0.001) >% 0 + rotation 1 0 0 1.5708 + size IS marker_size + name "back" + model IS marker + texture_url IS texture_url + } + Marker { + translation %<= fields.size.value.x / 2 + 0.001 >% 0 0 + rotation 0 1 0 1.5708 + size IS marker_size + name "side-1" + model IS marker + texture_url IS texture_url + } + Marker { + translation %<= -(fields.size.value.x / 2 + 0.001) >% 0 0 + rotation 0 1 0 -1.5708 + size IS marker_size + name "side-2" + model IS marker + texture_url IS texture_url + } + Marker { + translation 0 0 %<= fields.size.value.z / 2 + 0.001 >% + rotation 0 0 1 0 + size IS marker_size + name "top" + model IS marker + texture_url IS texture_url + } + Marker { + translation 0 0 %<= -(fields.size.value.z / 2 + 0.001) >% + rotation 0 1 0 3.1416 + size IS marker_size + name "bottom" + model IS marker + texture_url IS texture_url + } + # Shape { + # appearance PBRAppearance { + # transparency 0.4 + # baseColor 1 0 0 + # } + # geometry Sphere { + # radius %<= fields.size.value.x / 2 * 1.4 >% + # subdivision 5 + # } + # } + Connector { + type "passive" + distanceTolerance %<= fields.size.value.x / 2 * 1.4 >% + axisTolerance 3.1415 + rotationTolerance 0 + numberOfRotations 0 + tensileStrength IS connectorStrength + shearStrength IS connectorShear + snap FALSE + name "Front Connector" + } + Connector { + rotation 0 0 1 3.1416 + type "passive" + distanceTolerance %<= fields.size.value.x / 2 * 1.4 >% + axisTolerance 3.1415 + rotationTolerance 0 + numberOfRotations 0 + tensileStrength IS connectorStrength + shearStrength IS connectorShear + snap FALSE + name "Rear Connector" + } + ] + name IS model + model IS model + boundingObject USE TOKEN_GEOMETRY + physics Physics { + density -1 + mass IS mass + } + } +} diff --git a/simulator/protos/props/Can.proto b/simulator/protos/props/Can.proto new file mode 100755 index 0000000..4d549e9 --- /dev/null +++ b/simulator/protos/props/Can.proto @@ -0,0 +1,36 @@ +#VRML_SIM R2023b utf8 + +PROTO Can [ + field SFVec3f translation 0 0 0.001 + field SFRotation rotation 1 0 0 0 + field SFString name "" +] { + Solid { + translation IS translation + rotation IS rotation + children [ + Shape { + appearance DEF TOKEN_APPEARANCE PBRAppearance { + baseColor 1 1 1 + roughness 1 + metalness 1 + } + geometry DEF TOKEN_GEOMETRY Cylinder { + height 0.1 + radius 0.034 + subdivision 12 + } + } + ] + name IS name + boundingObject USE TOKEN_GEOMETRY + physics Physics { + density -1 + mass 0.300 + damping Damping { + linear 0.4 + angular 0.4 + } + } + } +} \ No newline at end of file diff --git a/simulator/protos/props/Marker.proto b/simulator/protos/props/Marker.proto new file mode 100755 index 0000000..f890f33 --- /dev/null +++ b/simulator/protos/props/Marker.proto @@ -0,0 +1,99 @@ +#VRML_SIM R2022b utf8 +# template language: javascript +# tags: nonDeterministic + +PROTO Marker [ + field SFVec3f translation 0 0 0 + field SFRotation rotation 0 1 0 0 + field SFVec2f {0.08 0.08, 0.1 0.1, 0.15 0.15, 0.2 0.2} size 0.08 0.08 + field SFString name "" + field SFString model "" + field MFString texture_url [] + field SFBool add_recognition FALSE + field SFBool upright FALSE +] +{ + %< if (fields.upright.value) { >% + Pose { + translation IS translation + rotation IS rotation + children [ + %< } >% + Solid { + %< + import * as wbrandom from 'wbrandom.js'; + wbrandom.seed(context.id); + const uid = wbrandom.integer(); + >% + %< if (fields.upright.value) { >% + rotation 1 0 0 1.5708 + %< } else { >% + translation IS translation + rotation IS rotation + %< } >% + children [ + %< + if (fields.add_recognition.value) { + for (let corner of [['TL', 1, 1], ['TR', -1, 1], ['BR', -1, -1], ['BL', 1, -1]]) { + let corner_name = corner[0]; + let horiz_sign = corner[1]; + let vert_sign = corner[2]; + >% + Solid { + translation %<= horiz_sign * fields.size.value.x / 2 >% %<= vert_sign * fields.size.value.y / 2 >% 0.001 + children [ + Shape { + appearance PBRAppearance { + transparency 1 + metalness 0 + } + geometry Plane { + # Make the detection corners one marker pixel in size + size %<= fields.size.value.x / 8 >% %<= fields.size.value.y / 8 >% + } + castShadows FALSE + } + ] + model %<= "\"" + uid + "_" + fields.model.value + "_" + corner_name + "\"" >% + name %<= "\"" + corner_name + "\"" >% + locked TRUE + recognitionColors [ + 0 0 1 + ] + } + %< }} >% + Shape { + appearance PBRAppearance { + baseColorMap ImageTexture { + url IS texture_url + repeatS FALSE + repeatT FALSE + } + roughness 1 + metalness 0 + } + geometry Plane { + # The size of the marker including the white border + size %<= 1.25 * fields.size.value.x >% %<= 1.25 * fields.size.value.y >% + } + castShadows FALSE + } + ] + %< if (fields.name.value !== "") { >% + name IS name + %< } else { >% + name IS model + %< } >% + model %<= "\"" + uid + "_" + fields.model.value + "_base\"" >% + locked TRUE + %< if (fields.add_recognition.value) { >% + recognitionColors [ + 1 1 1 + ] + %< } >% + } + %< if (fields.upright.value) { >% + ] + } + %< } >% +} diff --git a/simulator/protos/robot/BumpSensor.proto b/simulator/protos/robot/BumpSensor.proto new file mode 100755 index 0000000..c2b8927 --- /dev/null +++ b/simulator/protos/robot/BumpSensor.proto @@ -0,0 +1,28 @@ +#VRML_SIM R2023b utf8 +# template language: javascript + +PROTO BumpSensor [ + field SFString name "" + field SFVec3f translation 0 0 0 + field SFRotation rotation 0 0 1 0 +] { + TouchSensor { + translation IS translation + rotation IS rotation + name IS name + children [ + Shape { + appearance PBRAppearance { + baseColor 0.5 0 0 + roughness 0.7 + } + geometry DEF BUMPER Box { + size 0.01 0.05 0.01 + } + } + ] + boundingObject Box { + size 0.03 0.05 0.01 + } + } +} \ No newline at end of file diff --git a/simulator/protos/robot/Caster.proto b/simulator/protos/robot/Caster.proto new file mode 100755 index 0000000..a1331c7 --- /dev/null +++ b/simulator/protos/robot/Caster.proto @@ -0,0 +1,52 @@ +#VRML_SIM R2023b utf8 + +PROTO Caster [ + field SFString name "" + field SFVec3f translation 0 0 0 + field SFRotation rotation 0 0 1 0 +] { + Pose { + translation IS translation + rotation IS rotation + children [ + Solid { + name IS name + children [ + DEF CASTER_BALL Shape { + appearance PBRAppearance { + baseColor 0 0.0051864 0 + roughness 0 + metalness 0 + } + geometry Sphere { + radius 0.005 + } + castShadows FALSE + } + ] + boundingObject USE CASTER_BALL + physics Physics { + density 8000 # steel + } + } + Pose { + translation 0 0 0.01075 + children [ + DEF CASTER_TOP_CYLINDER Shape { + appearance PBRAppearance { + baseColor 0 0.0051864 0 + roughness 0.5 + metalness 0 + } + geometry Cylinder { + height 0.0215 + radius 0.01 + subdivision 6 + top FALSE + } + } + ] + } + ] + } +} \ No newline at end of file diff --git a/simulator/protos/robot/Flag.proto b/simulator/protos/robot/Flag.proto new file mode 100755 index 0000000..f4f039c --- /dev/null +++ b/simulator/protos/robot/Flag.proto @@ -0,0 +1,51 @@ +#VRML_SIM R2023b utf8 +# template language: javascript + +PROTO Flag [ + field SFString name "" + field SFVec3f translation 0 0 0 + field SFRotation rotation 0 0 1 0 + field SFColor flagColour 0.1 0.1 0.7 +] { + Solid { + translation IS translation + rotation IS rotation + name IS name + children [ + Shape { # pole + appearance PBRAppearance { + baseColor 0.0705882 0.0705882 0.0705882 + roughness 0.4 + metalness 0 + } + geometry Cylinder { + height 0.2 + radius 0.0075 + subdivision 8 + bottom FALSE + } + } + Solid { + translation -0.08 0 0.04925 + children [ + Shape { + appearance PBRAppearance { + metalness 0 + baseColor IS flagColour + } + geometry Box { + size 0.15 0.005 0.1 + } + } + ] + name %<= "\"" + fields.name.value + "flag\"" >% + } + ] + boundingObject Box { + size 0.015 0.015 0.2 + } + physics Physics { + density 1000 + } + } +} \ No newline at end of file diff --git a/simulator/protos/robot/MotorAssembly.proto b/simulator/protos/robot/MotorAssembly.proto new file mode 100755 index 0000000..b73b13b --- /dev/null +++ b/simulator/protos/robot/MotorAssembly.proto @@ -0,0 +1,85 @@ +#VRML_SIM R2023b utf8 +# template language: javascript + +PROTO MotorAssembly [ + field SFString name "" + field SFVec3f translation 0 0 0 + field SFRotation rotation 0 0 1 0 + field SFFloat maxVelocity 25 + field SFBool reversed FALSE +] { + Pose { + translation IS translation + rotation IS rotation + children [ + Solid { + translation 0 0.04 0 + rotation -1 0 0 1.5708 + name %<= "\"" + fields.name.value + " housing\"" >% + children [ + Shape { + appearance PBRAppearance { + baseColor 0.36 0.36 0.36 + roughness 0.3 + metalness 0 + } + geometry Cylinder { + height 0.07 + radius 0.015 + subdivision 12 + } + } + ] + boundingObject Box { + size 0.03 0.03 0.07 + } + physics Physics { + density 8000 # steel + } + } + HingeJoint { + jointParameters HingeJointParameters { + position 0 + %< if (fields.reversed.value) { >% + axis 0 -1 0 + %< } else { >% + axis 0 1 0 + %< } >% + } + device [ + RotationalMotor { + name IS name + maxVelocity IS maxVelocity + sound "" + } + PositionSensor { + name %<= "\"" + fields.name.value + " sensor\"" >% + } + ] + endPoint Solid { + translation 0 0 0 + rotation -1 0 0 1.5708 + children [ + DEF WHEEL_GEO Shape { + appearance PBRAppearance { + baseColor 0 0.0051864 0 + roughness 0.3 + metalness 0 + } + geometry Cylinder { + height 0.021 + radius 0.05 + subdivision 24 + } + } + ] + name IS name + boundingObject USE WHEEL_GEO + physics Physics { + density 2000 + } + } + } + ] + } +} \ No newline at end of file diff --git a/simulator/protos/robot/RGBLed.proto b/simulator/protos/robot/RGBLed.proto new file mode 100755 index 0000000..ef58afd --- /dev/null +++ b/simulator/protos/robot/RGBLed.proto @@ -0,0 +1,37 @@ +#VRML_SIM R2023b utf8 + +PROTO RGBLed [ + field SFString name "" + field SFVec3f translation 0 0 0 + field SFRotation rotation 0 0 1 0 +] { + LED { + name IS name + translation IS translation + rotation IS rotation + children [ + Shape { + appearance DEF APP_LED PBRAppearance { + baseColor 0.6 0.4 0.4 + roughness 1 + emissiveIntensity 100 + } + geometry Box { + size 0.02 0.04 0.005 + } + castShadows FALSE + } + ] + color [ + 1 0 0 # RED + 1 1 0 # YELLOW + 0 1 0 # GREEN + 0 1 1 # CYAN + 0 0 1 # BLUE + 1 0 1 # MAGENTA + 1 1 1 # WHITE + ] + } +} + + diff --git a/simulator/protos/robot/ReflectanceSensor.proto b/simulator/protos/robot/ReflectanceSensor.proto new file mode 100755 index 0000000..14c33a4 --- /dev/null +++ b/simulator/protos/robot/ReflectanceSensor.proto @@ -0,0 +1,40 @@ +#VRML_SIM R2023b utf8 + +PROTO ReflectanceSensor [ + field SFString name "" + field SFVec3f translation 0 0 0 + field SFRotation rotation 0 0 1 0 +] { + Pose { + translation IS translation + rotation IS rotation + children [ + DistanceSensor { + name IS name + rotation 0 1 0 1.5708 + type "infra-red" + lookupTable [ + # 2% standard deviation + 0 0 0.02 + 0.1 1023 0.02 + ] + children [ + Pose { + children [ + Shape { + appearance PBRAppearance { + baseColor 0.1 0.1 1 + metalness 0 + } + geometry Box { + size 0.002 0.045 0.015 + } + castShadows FALSE + } + ] + } + ] + } + ] + } +} diff --git a/simulator/protos/robot/RobotCamera.proto b/simulator/protos/robot/RobotCamera.proto new file mode 100755 index 0000000..dae8afc --- /dev/null +++ b/simulator/protos/robot/RobotCamera.proto @@ -0,0 +1,56 @@ +#VRML_SIM R2023b utf8 + +PROTO RobotCamera [ + field SFString name "" + field SFVec3f translation 0 0 0 + field SFRotation rotation 0 0 1 0 + field SFInt32 width 800 + field SFInt32 height 450 +] { + Camera { + name IS name + translation IS translation + rotation IS rotation + children [ + Pose { + translation 0 0 0 + rotation 0 1 0 1.5708 + children [ + Shape { + appearance PBRAppearance { + baseColor 0 0 0 + } + geometry Cylinder { + height 0.01 + radius 0.01 + } + castShadows FALSE + } + ] + translationStep 0.001 + } + Pose { + translation -0.02 0 0 + children [ + Shape { + appearance PBRAppearance { + baseColor 0.4 0.4 0.4 + metalness 0 + } + geometry Box { + size 0.03 0.03 0.03 + } + } + ] + } + ] + # In radians, ~45 degrees + fieldOfView 0.82 + width IS width + height IS height + recognition Recognition { + frameThickness 2 + maxRange 6 + } + } +} diff --git a/simulator/protos/robot/UltrasoundModule.proto b/simulator/protos/robot/UltrasoundModule.proto new file mode 100755 index 0000000..0f93f37 --- /dev/null +++ b/simulator/protos/robot/UltrasoundModule.proto @@ -0,0 +1,75 @@ +#VRML_SIM R2023b utf8 +# template language: javascript + +PROTO UltrasoundModule [ + field SFString name "" + field SFVec3f translation 0 0 0 + field SFRotation rotation 0 0 1 0 + field SFInt32 range 4 + field SFBool upright FALSE +] { + DistanceSensor { + name IS name + translation IS translation + rotation IS rotation + type "sonar" + numberOfRays 10 + aperture 0.3 + lookupTable [ + # 1% standard deviation, with no deviation at the limits + 0 0 0 + 0.01 10 0.01 + %<= fields.range.value * 0.99 >% %<= fields.range.value * 1000 * 0.99 >% 0.01 + %<= fields.range.value >% %<= fields.range.value * 1000 >% 0 + # Return 0 for out of range values + %<= fields.range.value + 0.001 >% 0 0 + ] + children [ + Pose { + %< if (fields.upright.value) { >% + rotation 0 1 1 3.14159 + %< } else { >% + rotation 0 0 1 3.14159 + %< } >% + children [ + Shape { + appearance PBRAppearance { + baseColor 0.1 0.1 1 + metalness 0 + } + geometry Box { + size 0.002 0.045 0.02 + } + } + Pose { + translation -0.007 0.013 0 + rotation 0 -1 0 1.5708 + children [ + DEF SONAR_TRX Shape { + appearance PBRAppearance { + baseColor 0.92 0.92 0.92 + roughness 0.3 + } + geometry Cylinder { + radius 0.008 + height 0.012 + subdivision 12 + bottom FALSE + } + castShadows FALSE + } + ] + } + Pose { + translation -0.007 -0.013 0 + rotation 0 -1 0 1.5708 + children [ + USE SONAR_TRX + ] + } + ] + } + ] + } +} + diff --git a/simulator/protos/robot/VacuumSucker.proto b/simulator/protos/robot/VacuumSucker.proto new file mode 100644 index 0000000..6baffca --- /dev/null +++ b/simulator/protos/robot/VacuumSucker.proto @@ -0,0 +1,201 @@ +#VRML_SIM R2023b utf8 +# template language: javascript + +PROTO VacuumSucker [ + field SFString name "" + field SFVec3f translation 0 0 0 + field SFRotation rotation 0 0 1 0 + field SFFloat max_height 0.35 + field SFFloat forward_reach 0.12 +] { + Pose { + translation IS translation + rotation IS rotation + children [ + Pose { + translation 0 0 %<= fields.max_height.value / 4 >% + children [ + DEF BASE Solid { + children [ + Shape { + appearance PBRAppearance { + baseColor 0.7 0.7 0.7 + roughness 1 + metalness 0 + } + geometry DEF BASE_GEO Box { + size 0.06 0.06 %<= fields.max_height.value / 2 >% + } + } + ] + name "Tower Base" + boundingObject USE BASE_GEO + physics Physics { + density 500 # Hollow Aluminium + } + } + SliderJoint { + jointParameters JointParameters { + axis 0 0 1 + minStop 0 + maxStop IS max_height + position 0.01 + } + device [ + LinearMotor { + name %<= "\"" + fields.name.value + " motor::main\"" >% + minPosition 0 + maxPosition IS max_height + maxVelocity 2 + sound "" + } + PositionSensor { + name %<= "\"" + fields.name.value + " position sensor\"" >% + } + ] + endPoint Solid { + translation %<= (fields.forward_reach.value + 0.055) - 0.05 >% 0 %<= fields.max_height.value / 4 - 0.05 >% + children [ + Solid { + children [ + Pose { + translation %<= 0.025 - (fields.forward_reach.value + 0.055) / 2 >% 0 0.08 + children [ + Shape { # Horizontal arm + appearance PBRAppearance { + baseColor 0.7 0.7 0.7 + roughness 1 + metalness 0 + } + geometry Box { + size %<= fields.forward_reach.value + 0.055 >% 0.05 0.02 + } + } + ] + } + Pose { + translation %<= 0.05 - (fields.forward_reach.value + 0.055) >% 0 %<= 0.06 - (fields.max_height.value / 4) >% + children [ + Shape { # Column + appearance PBRAppearance { + baseColor 0.7 0.7 0.7 + roughness 1 + metalness 0 + } + geometry DEF COLUMN_GEO Box { + size 0.05 0.05 %<= fields.max_height.value / 2 + 0.02 >% + } + } + ] + } + Pose { + translation 0 0 0.045 + children [ + Shape { + appearance PBRAppearance { + baseColor 0.7 0.7 0.7 + roughness 1 + metalness 0 + } + geometry Box { + size 0.05 0.05 0.05 + } + } + ] + } + ] + name "Armature" + boundingObject Pose { + translation %<= 0.05 - (fields.forward_reach.value + 0.055) >% 0 %<= 0.06 - (fields.max_height.value / 4) >% + children [USE COLUMN_GEO] + } + physics Physics { + density 200 # Hollow Aluminium + } + } + DEF SUCKER Shape { + appearance PBRAppearance { + baseColor 0 0 0 + roughness 1 + metalness 1 + } + geometry DEF hook_geo Box { + size 0.05 0.05 0.04 + } + castShadows FALSE + } + Connector { + # Shift origin to near the lower face of the hook shape + translation 0 0 -0.01 + rotation 0 1 0 3.1416 + type "active" + distanceTolerance 0.094 # (0.14 / 2) * sqrt(2) + axisTolerance 3.1415 + rotationTolerance 0 + numberOfRotations 0 + tensileStrength 35 + shearStrength 20 + snap FALSE + autoLock TRUE + name IS name + unilateralUnlock TRUE + unilateralLock TRUE + } + ] + boundingObject Pose { + translation 0 0 0.005 + children [ + Box { + size 0.05 0.05 0.03 + } + ] + } + physics Physics { + density 1500 # rubber + } + } + } + # Secondary slider joint for telescoping column + SliderJoint { + jointParameters JointParameters { + axis 0 0 1 + minStop 0 + maxStop %<= fields.max_height.value / 2 >% + position 0.005 + } + device [ + LinearMotor { + name %<= "\"" + fields.name.value + " motor::extension\"" >% + minPosition 0 + maxPosition %<= fields.max_height.value / 2 >% + maxVelocity 1 + sound "" + multiplier 0.5 + } + ] + endPoint Solid { + translation 0 0 0.005 + children [ + Shape { + appearance PBRAppearance { + baseColor 0.7 0.7 0.7 + roughness 1 + metalness 0 + } + geometry DEF COLUMN_GEO Box { + size 0.055 0.055 %<= fields.max_height.value / 2 + 0.01 >% + } + } + ] + name "Extension" + boundingObject USE COLUMN_GEO + physics Physics { + density 200 # Hollow Aluminium + } + } + } + ] + } + ] + } +} \ No newline at end of file diff --git a/simulator/worlds/.arena.wbproj b/simulator/worlds/.arena.wbproj new file mode 100644 index 0000000..eacebd7 --- /dev/null +++ b/simulator/worlds/.arena.wbproj @@ -0,0 +1,10 @@ +Webots Project File version R2025a +perspectives: 000000ff00000000fd00000002000000010000011c0000036dfc0200000001fb0000001400540065007800740045006400690074006f007201000000160000036d0000003f00ffffff00000003000007800000009dfc0100000001fb0000001a0043006f006e0073006f006c00650041006c006c0041006c006c0100000000000007800000006900ffffff000006620000036d00000001000000020000000100000008fc00000000 +simulationViewPerspectives: 000000ff000000010000000200000100000001a20100000002010000000100 +sceneTreePerspectives: 000000ff00000001000000030000001f000002f4000000fa0100000002010000000200 +maximizedDockId: -1 +centralWidgetVisible: 1 +orthographicViewHeight: 1 +textFiles: -1 +consoles: Console:All:All +renderingDevicePerspectives: robot0:camera;1;0.497778;0;0 diff --git a/simulator/worlds/arena.wbt b/simulator/worlds/arena.wbt new file mode 100755 index 0000000..acf9bcc --- /dev/null +++ b/simulator/worlds/arena.wbt @@ -0,0 +1,289 @@ +#VRML_SIM R2023b utf8 +EXTERNPROTO "https://raw.githubusercontent.com/cyberbotics/webots/R2023b/projects/objects/backgrounds/protos/TexturedBackgroundLight.proto" +EXTERNPROTO "../protos/arena/Arena.proto" +EXTERNPROTO "../protos/arena/Deck.proto" +EXTERNPROTO "../protos/arena/TriangleDeck.proto" +EXTERNPROTO "../protos/props/Can.proto" +EXTERNPROTO "../protos/props/Marker.proto" +EXTERNPROTO "../protos/SRObot.proto" + +WorldInfo { + basicTimeStep 8 +} +Viewpoint { + orientation 0.43231 0.43231 -0.79134 1.80272 + position 0 9.1 13.8 +} +DEF AMBIENT Background { + skyColor [ + 0.4 0.4 0.4 + ] + luminosity 1.6 +} +TexturedBackgroundLight { +} + +DEF ROBOT0 SRObot { + name "robot0" + translation 0.45 1.95 0 + rotation 0 0 1 3.1415 + flagColour 0 1 0 + controllerArgs ["0"] + controller "usercode_runner" + customData "start" +} + +DEF ROBOT1 SRObot { + name "robot1" + translation -0.45 -1.95 0 + rotation 0 0 1 0 + flagColour 1 0.4 0 + controllerArgs ["1"] + controller "usercode_runner" + customData "start" +} + +Robot { + name "competition_supervisor" + controller "competition_supervisor" + supervisor TRUE +} + +Arena { + size 5.4 5.4 + locked TRUE + floorTexture ["arena_floor.png"] +} +Deck { + name "TL deck" + size 1.2192 1.2192 + translation 2.0904 -2.0904 0.085 + locked TRUE +} +Deck { + name "BR deck" + size 1.2192 1.2192 + translation -2.0904 2.0904 0.085 + locked TRUE +} +Deck { + name "CR deck" + size 1.2192 1.2192 + translation -0.6096 -0.6096 0.085 + locked TRUE +} +Deck { + name "CL deck" + size 1.2192 1.2192 + translation 0.6096 0.6096 0.085 + locked TRUE +} +TriangleDeck { + name "TR deck" + size 1.2192 1.2192 + translation -2.0904 -2.0904 0.085 + locked TRUE +} +TriangleDeck { + name "BL deck" + size 1.2192 1.2192 + translation 2.0904 2.0904 0.085 + rotation 0 0 1 3.14159 + locked TRUE +} + +DEF CANS Pose { + translation 0 0 0.05 + children [ + Can { + name "can1" + translation -0.46 0.60 0 + } + Can { + name "can2" + translation -1.48 1.1 0 + } + Can { + name "can3" + translation -1.5892 0.06 0 + } + Can { + name "can4" + translation -2.33 -0.06 0 + } + Can { + name "can5" + translation -1.48 -1.95 0 + } + + Can { + name "can6" + translation 0.46 -0.6 0 + } + Can { + name "can7" + translation 1.48 -1.1 0 + } + Can { + name "can8" + translation 1.5892 -0.06 0 + } + Can { + name "can9" + translation 2.33 0.06 0 + } + Can { + name "can10" + translation 1.48 1.95 0 + } + ] +} + +Marker { + name "A0" + model "100" + size 0.1 0.1 + translation -1.4798 2.165 0.07 + rotation 0 0 1 1.5708 + texture_url ["sim_markers/100.png"] + upright TRUE +} +Marker { + name "A1" + model "101" + size 0.1 0.1 + translation -1.4798 2.015 0.07 + rotation 0 0 1 1.5708 + texture_url ["sim_markers/101.png"] + upright TRUE +} +Marker { + name "A2" + model "102" + size 0.1 0.1 + translation -0.535 0.001 0.07 + rotation 0 0 1 3.1415 + texture_url ["sim_markers/102.png"] + upright TRUE +} +Marker { + name "A3" + model "103" + size 0.1 0.1 + translation -0.685 0.001 0.07 + rotation 0 0 1 3.1415 + texture_url ["sim_markers/103.png"] + upright TRUE +} +Marker { + name "A4" + model "104" + size 0.1 0.1 + translation -2.699 0.815 0.07 + rotation 0 0 1 1.5708 + texture_url ["sim_markers/104.png"] + upright TRUE +} +Marker { + name "A5" + model "105" + size 0.1 0.1 + translation -2.699 0.665 0.07 + rotation 0 0 1 1.5708 + texture_url ["sim_markers/105.png"] + upright TRUE +} +Marker { + name "A6" + model "106" + size 0.1 0.1 + translation -2.14243 -2.03637 0.07 + rotation 0 0 1 2.3561 + texture_url ["sim_markers/106.png"] + upright TRUE +} +Marker { + name "A7" + model "107" + size 0.1 0.1 + translation -2.03637 -2.14243 0.07 + rotation 0 0 1 2.3561 + texture_url ["sim_markers/107.png"] + upright TRUE +} + +Marker { + name "B0" + model "100" + size 0.1 0.1 + translation 1.4798 -2.165 0.07 + rotation 0 0 1 -1.5708 + texture_url ["sim_markers/100.png"] + upright TRUE +} +Marker { + name "B1" + model "101" + size 0.1 0.1 + translation 1.4798 -2.015 0.07 + rotation 0 0 1 -1.5708 + texture_url ["sim_markers/101.png"] + upright TRUE +} +Marker { + name "B2" + model "102" + size 0.1 0.1 + translation 0.535 -0.001 0.07 + rotation 0 0 1 0 + texture_url ["sim_markers/102.png"] + upright TRUE +} +Marker { + name "B3" + model "103" + size 0.1 0.1 + translation 0.685 -0.001 0.07 + rotation 0 0 1 0 + texture_url ["sim_markers/103.png"] + upright TRUE +} +Marker { + name "B4" + model "104" + size 0.1 0.1 + translation 2.699 -0.815 0.07 + rotation 0 0 1 -1.5708 + texture_url ["sim_markers/104.png"] + upright TRUE +} +Marker { + name "B5" + model "105" + size 0.1 0.1 + translation 2.699 -0.665 0.07 + rotation 0 0 1 -1.5708 + texture_url ["sim_markers/105.png"] + upright TRUE +} +Marker { + name "B6" + model "106" + size 0.1 0.1 + translation 2.14243 2.03637 0.07 + rotation 0 0 1 -0.7853 + texture_url ["sim_markers/106.png"] + upright TRUE +} +Marker { + name "B7" + model "107" + size 0.1 0.1 + translation 2.03637 2.14243 0.07 + rotation 0 0 1 -0.7853 + texture_url ["sim_markers/107.png"] + upright TRUE +} + +# 5400/2 - 1219.2/2 ± 150/2/sqrt(2) +# 2.14343, 2.03737 diff --git a/simulator/worlds/arena_floor.png b/simulator/worlds/arena_floor.png new file mode 100644 index 0000000000000000000000000000000000000000..62abce7adadcbf190dfa8ff36b4c9f971c3831bb GIT binary patch literal 154048 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 literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/0.png b/simulator/worlds/sim_markers/0.png new file mode 100644 index 0000000000000000000000000000000000000000..5d985b14a47e459f23fe613e974f31365c7569b3 GIT binary patch literal 3255 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyou`XqNX4AD*AC`BFyLW0 zc-isq_BSSp6FSS6F$PuXRLrY)xcdJsYlAHlL*ghRG6c%g&dii#p8v7Or;yQM2E!;K zAp~yRje8J!{u@)im2z;}zMN;e_p8qEh%>Zsjv}H%;O<-2xKg)eHp;=vj!k&R)L_6k zibx274ZHU;>^=ufg1@dLr|n=+@EJu!hrpXW{ST|sQ%*_FH2NZOD~3TpZ4?n50%hC9 zE3Qh;{M6&~^ICG+nVOr+M^hiEnGciz+2_7x{U;ayEqW)>>6TP$hNXI7*d186c`w82 u(R@p4u~jzjEpLOV9s`H)C?YxpelyOt5)qt~@%{s-N8;(~=d#Wzp$P!|77`}_ literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/1.png b/simulator/worlds/sim_markers/1.png new file mode 100644 index 0000000000000000000000000000000000000000..b04880dd5244469731b57771429960f9d0dec4bf GIT binary patch literal 3266 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyr>Bc!NX4AD*AC_~8}cw5 z)Cl;m8?W`@aD&6qdo$TiOzvg9{+T)C@4YyN9qtSYKBI`Z5Xd>-6KS0MAlpVcIEFz$ zZ4{9b0^9QS3$7GbH%`8GLd5^tGo}Uu#!*CU2yCdWZd`or#FX?DuJ^Nlrl$Q%bskND zq-H>P3f%WH`Py67ii4~SiAlnuSRz!N6H~-bBsp zlv5XuS8UqOTYH8_oS}tt6p<1FZ*OzUY|YdEP-zo6o15Vf%P1l`1ik@_ywtmK4?Y7E z-%jEhJfkf`cnIX!ZZ~IGSIFovgJBdA8v-}v9T$rUlm|_g0d-D1UHx3vIVCg!0Jm^+ A761SM literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/10.png b/simulator/worlds/sim_markers/10.png new file mode 100644 index 0000000000000000000000000000000000000000..9f9d020bfe314799aa2a3e001247384f9fe038e6 GIT binary patch literal 3267 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwym#2$kNX4AD*ADjHFyLW0 zs8R4=S6lSXc?JD@Gub?D9%y?0k8#z%Jbi{Yf(#tOqlmZ=s7^aJXQtr?$?}vgZiYiF zqllCcnD>_T$H}{K4_wpJ&YV$~t~$db&d|a+iii#Y+uPg$1>40RuZ$Ql~vyiI7x>0xOXX zuXFSdXpM#&N#O=mJa_kA#tp|<84{UB5z!&=pW(c^sKA?K`3g|)#M9N!Wt~$(69Db> Bd0_wm literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/100.png b/simulator/worlds/sim_markers/100.png new file mode 100644 index 0000000000000000000000000000000000000000..9dfba3d79c06c896a3da990f908663f17766eeb1 GIT binary patch literal 3259 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwylc$SgNX4AD*AC_$G2mf1 zs1fjAS6lSXW4n+yb2A&J@apV1#~<*ux|;EU9AksQC?YNdHrN`co$YD7ZlN3;!yuqG zibx58ZTb2i9MaRyNX<0*JY(ke54PeAEu5o>=n!~&o7?8XEXm@uV?7m@qnG{r&odf; zq^2KW7=Ab@HM7WV*@yU@3<^G@i0BZ=xgYnS@!XjiUl(aQ&+l138hRup9&qSgJ2S)B zDD|1)`d=yQA2E$qyQGD{hT7^zb}QrLYiA@bjn=*-H9o*q@9S?&H4o0$oMmN5WEw@J mguv@N_ZW5@8x1#-l1+VRl&HXzMT$Xd%gG%{<+t=?`(Pntmf6^ zkJn=u1k^?m(IK!cUcca~)XXBcWjr5G-Wm-&QqvAN{3K^?0*0S$o0&L63+E^zIt1Qc z=Z@J5s)Cox9kxF;TJVut?!Vr1kD=n&Xtl#+wzs=3?or9r;a}*I50&iy;W~cG&IVL&t5mSQ! z<0v8_1UA%GH@eTAsF|H|YC?GJ&DC&ap z{5wCIhe;~JfO)td9DFx`8T!$rlWb4q@erF?93)A;Ir>nC#YBA>FVdQ&MBb@08X$2(f|Me literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/103.png b/simulator/worlds/sim_markers/103.png new file mode 100644 index 0000000000000000000000000000000000000000..85fe7a4e5cb8771669ea3b83335552505bda3ccd GIT binary patch literal 3271 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwypQnpsNX4AD*AC_$G2mf1 zs1fjAS6lQ+dO-QTnQR`X4>Z00!w|ZEyEwx;K?V-tQAAt_+%`_Wc1EJ2*VyPND?=jF zC?X{Uet%=?w^9zic4CU6vA@}q(E7ACZiYiFqloAb`1Y1nBf>cO+=(d*`)596YA|3N zMI?m4hT7^z_q7u>(^F1On5q6acqfB`&nO}~1aj`jHT++5o7?8aow$bj9YpIJ4P#&k z00ZwqATZ%c%`{4RcCu~HtMg|H7#(IXj3N?3;D(iPvfDDAnU$Nu_ujlP`K-h*L*x8s zqvaqe)c~jv)Zdb)|Do3A=JKM^BuHWoykWK7oMBzzXtLw* z7*p_H*IM++kIpR3K<<{ zFpMG+Lg2>My$2S*o2Z$da%#1l@#krZ&j0J%xET(yj3S~#VBK3*xzcUo2~+?7Hxy@R z;T%Pzgh1?V?hUWyW)`_E`(Qa5dL$(tV9fy5Cg}K6hfu!sB~hP3G;b6dujOq-J58S2_A0mW123 z*`H!%NMssCq=dlpd-oV39*^c+;+o2kKE($4o<>o@eHj@yKz$QWS3j3^P6V7}fp2pSU!Rkx=q*m|;vNk) z5<}qI7p8uz%iK1ZzWNRgqrpdHl7lD%hTZ=QyNsX3%q)6oSpFj=zL?Qr2E!;KAp~yB zi)-+|_JxVxs{GiVmy=^3F*O)4jv``1V1w;mhRx@`F!}HLJfk%Al&T&BhwvyOIs|TC z<__4fO#A_}I718PC?X{U-o9j&+qz7=;?ho|r{~mnf1XzU_rG-vgMiv7B02=NEf;?< z*>vxL=6heJl^2fI9VAwrH>@s8GbE3uIg+y6H}CAlJFuKa>m-@h>(uw`OM97RNiKy})gS(420k9&Ly869RY zj3N?3;Ksha2i)gQ)XYvf6+ZK)g#G6^+zf|UMiJ2=@a-*Y+>1ML40cCZN3$%6A@KVf zll)eZ%pquryct-^Z01!k(zjb0eB!bJ>`_t%tOU`3>?Cvi0BZwy=xMX#~ZQC5aT zrcp#n2>kxW)Ni$2yyBA7%ulQ0&61xbw(Ve0@EJu!hd|E#xCeo+zcIx;{cHb>slkA8 z6p;`D8)~Z?-RDl!%uYEqVP^l#N{RRXEyWpHI7boDA@KG#cg;d$Hchf{Xi8OfPOpAC)gA9^0cAfPsi zhz@~m`T7Uyuf1iJ`?4&*Qhc=ZBen2fTU^cjKx#DHND8;VjKR~y1eC8gzXtVCJYD@< J);T3K0RXii_yzy~ literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/108.png b/simulator/worlds/sim_markers/108.png new file mode 100644 index 0000000000000000000000000000000000000000..407106fb1f1cbe583d20a801f0dc4055d81ce699 GIT binary patch literal 3266 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyr>Bc!NX4AD*BUdAIPfqW zj4Al9Yi(*Hq4{rarh}EV*xcXjD}LRLW4NKrAfPsihzo(Tvptr^$q%ycCEpnV(|&XFg6z?CM7MyUM}mPE1*N{LGJK+iS$d8Cp0; z5h)=Mdz;(lRgce3_hkndMnjLJ!~+hxdvOnH&wXR^|McJf*=Y79rSt-4-??vD?>z^G zq36H<$49FVc)dU*16X~0aQY5ReOAiBZFV=$%$VtKc7&B7k!chW9RkBVFQ2*^qL-Vz{#__*WrdKdJ z%wQNrB!s{XW7`KSY;7OZUi;E8+3sudyAMna28^SK*bvx|UUeWayXt`FZ|88mpI83Z zw~I5haE>CPL%{YjbKkA-b!)W4^X4j^=$*J`QGu-;3FyZ z0K?9G?Mue@k0)=f9IZJ>tT=Dny3E~R7{?%>Hj0Q2fp6^Z(?kUF_O5*i>Y8}E`njxg HN@xNA?hK?a literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/11.png b/simulator/worlds/sim_markers/11.png new file mode 100644 index 0000000000000000000000000000000000000000..4f8a22ad9984f5364f2d7af75c9cf8289b2688c3 GIT binary patch literal 3262 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwytEY=&NX4AD*AC_?JMu6b zoPGG&{7)8)zttyPC<|qJlC^;C{9pEvzxU%9ZYVPdsEs1xLg3q(9!ulo2d3{%h=?<^ zaE>BULcsPm_lN1b_cH9azRg`TiJRdN%P1l`1m?YEwR@GP&v5@JD?=jFC?X{U&VOTy zf03jA!LvN&)CGUD&xY$CF*O)4jv``1U_*K}W7*t^nxODIzNc$6>ylJ*0kiJD3v(rl z(~j*qdFJ$yezv z^xp9E`4UEl84RO{gb=u~^R_fY_GrF^XGmfg-;V0bi@D86U_oHW-W|;zD3UdfJ&;lFaKjx-Z+opx`r# zNC|;AJNF(4{QZqdeeFa|4v0);#@!OL8+mtqMPOd^8o3 zlnTK~u(rB!^0gBp>(`uRWk_TiMWlp4_W2&knMNtkrhZ)8R`+b?{WGIMNKy(q|CaUN x!qRF+i=(3fM^eB66=&bS$FSqrXt#d$C1)M$Y^D4woXA@ zHi}3Ifo;p#ZC?5CSIjrHZ3xt3;1C`~M2Eob%gl0Hm$C0Tzw=WU|L5?X3<^G@h?Ee> z@#lYFo?dmJS?}+((&sbv3mF||FpMH%L*T|dxerdj;QPJiCF7q%tPF`vqloAbsIEG& z_}rILx&Obm&mh{zB)e`bcMFpZ#;XZ3lyb&nO}~1m5i2dm!-lHzxJ96E%(f z%|7eaoEuGoBqc$hVx&|kS+!~A{r{mc3<7GSi0Babb|>zEd=#(-eikUbi1B!$~I_7*iU{`Irkia|XTPgg&ebxsLQ0P!9h4FCWD literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/113.png b/simulator/worlds/sim_markers/113.png new file mode 100644 index 0000000000000000000000000000000000000000..b5e02a4703f86c22a503339691b0803311f6093f GIT binary patch literal 3270 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyucwP+NX4AD*AC`BFyLW0 zc-isq_BAFGmMb2+HtP$Fg`a`Q;#?u zqd^M~f!mk4_gt76_n`LLT*LBzXO{NwJj}|F$TW(G4uR^bM)$ee!Plm0PQO!W^iNBi zp@nl4krD!LU$V+=T_#>}X{XWCVE=C`N7Epw`45-|#jSuj`TX*EIllTIR7TSvNm&q- z087{TM20Vm`6c0h?HN;p0plnlAp|zq?mfUg_a*DRE$e)KUYT6m#m#VtWfTz|0^hzc z^+#Ri{$OqYY<&@kA@J=jt6XWG{)Z)|Mn@-a-N~TfGm3}~ft>qs4+67OPD#!*Ix-q~LQz!!fkI}IDV#p(P0L|C?YllZtUB8VDY)Ptn;?GEvvaY`Jh-Zy4qkR_!ZW4@1IAHALI`ZAt!@;L+Adx(-`M!r{GT&Mt6!2@AHcd-BVxPw y19?5-8aksbLU;&7KmW#dz;ra+-~mDm<2SR@Eiu0N7axXzIwhX2elF{r5}E+OR!Hyw literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/115.png b/simulator/worlds/sim_markers/115.png new file mode 100644 index 0000000000000000000000000000000000000000..4bafc9e62a6274a4d332e06872eec2b57e894265 GIT binary patch literal 3271 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwypQnpsNX4AD*DiJ*G2n4H z`1#?#=x?C~^4yMTpVOo!%sJrv`VT|s{_Wxn?*th*ghvr^A#mF``PvzYie6)*qpS>x zOrwaD5cvI#so!e5c*Uh%dk-|X)t#AbcT+%|p@nl45gh_=Z*$iyG&O$K<0DyhhG+hp zWIf{2?Pvyvhrn%M3cLbLfs@nZirtp+{GBoL_?|9qhC?i)i0Bab_Lfzy)NR?CiJH#+ z3(t<0gd|l2pkgpeIr!X(DGPxq&;Og@Xcsq_M7-yi39 y?PXvbtv%q?0+Ed9=ik^4n2v@Uk--H~_M7>2v8a%iQHVaMhvMn#=d#Wzp$Pz55$(JH literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/116.png b/simulator/worlds/sim_markers/116.png new file mode 100644 index 0000000000000000000000000000000000000000..cb1d23c7a9858bfeae90a5e03d321dcf3dc04b04 GIT binary patch literal 3263 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyo2QFoNX4AD*DmJXb>Lw* z7_;DC^ga`TdI< zS~y1$DIs8coBNMbd3EFc=Q;WZ#A6r))J74}A@D6n|HG=?dk@S$_l=2V2ZMsoC?X{U z-rS69XtFkbcBbd2iXL%!eY8-7gh2H+@rtXGGmG4p`SeN}eok()yLllwZ8Qmzlmmh3 z??EWA>@%M`QB(fw$scB;)d#7yC$Re1@Vdumr*d$c+~>g2>Vu@(6I^|4-pla+`SCv? uqXi(TWxI8GHFH8QH^U*8QABhI{9~LrQG{Rk#^DK|o{6WcpUXO@geCyfoWkh< literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/117.png b/simulator/worlds/sim_markers/117.png new file mode 100644 index 0000000000000000000000000000000000000000..2436087df9c119b99a260156e7f9e8c2d9ca8d5b GIT binary patch literal 3265 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyho_5UNX4AD*DiM6aNuz` zm{agyH(zTS+Gf%D&(*6&gdK6he@f9;tw+j%6b{!8jHa0rhgqC=p1n|Q@l$(cp&%W6)YIb1RG zelerN42DreLI~X0y!XKBcPFM88>PNTPFn^{g){XFM?;a6L<9^%27Alf+A%26=HRp`%isZ-A&nrrnhQNmU(?a5W_av4Dfchq$u6{1-oD!M< D4e5hX literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/118.png b/simulator/worlds/sim_markers/118.png new file mode 100644 index 0000000000000000000000000000000000000000..d2eef389e51731d108d9c860dc720b197893fc7e GIT binary patch literal 3260 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyv!{z=NX4AD*BU!-IPfqW zj4Al98=u7?@$$a)<%W`DI#%EL1HP75GbZ#iILsJD#D%~OW8-IMdK%B~P!5h^5KtRM zq=dk?9Q_Zgtc{-b_()cr=~?K1tiG6WH0hBL0yj4AJ+S)Ri7CJ^jP8HfUNjnd#N}Fe z@dgY)hTY%*OnqTE{}EGz0plnlHUu`LS2I?em7Q7Swv4B6H0hC&^uQVS-8ZKEUssaT z=DcOClNV=b;T%Pzgn;dB?me%7HOTKfZ&~k|e4gR|H&u^;LwFPs9Rk(c#1pRHjB5xT t4L4Ggjdgi7b3!jS!y%SYM05!JV+?pM$~Vt*uNJ6V;_2$=vd$@?2>@I}t=a$p literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/119.png b/simulator/worlds/sim_markers/119.png new file mode 100644 index 0000000000000000000000000000000000000000..ccd303db64654feef520f5ba697346b34cd6c979 GIT binary patch literal 3266 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyr>Bc!NX4AD*AC_~8}cwX zY6tw+)fW9B_`u=ly_swtcNw|YeP&u!m#5EgN0~uDZ4?m~0^8D$t(j@~!Svh-5pjkV z&QU~42)w<`U9-U0IIY)bX62bP(Lbz5gOH>Y1Pni$S3N$F%E9|y85-wH8h!bnzJo!* zXB3eV0y+2N9t6Jr#^gV%@{Ht6qc6#6a-TzEh|B(?8ow&m-8XgqgjhN*E{i~qOy zouq{SXmSOnz=o#MYQ}p7j1DsxMiB`iaAV(IhWB%T<=~6i_dZ2!7q6Ja&2Wfi6cHT) z-`=v?UD&mk!R9C{Ln6~CA|(V~-?_)I{d*Y-`WYN%j3VMf;Kn@3&1uIDq|ceCsmH(} zJc>vOf!o`~D;63Xr=9IlJj=?E$TW(G4uRj_nEI`N!FTCQ^0gCF7RskRGo1g3slkA8 z6p;`D8)~Z?-Phjcwt0Cc?t!DT{h2fBzpv~+Gny7j%8J0Ws9%zHY|TtVc;OM xPyW*qXK3LZMWlql+Tv>F2U4TqMpCl*%aAE6%J2Ezw;t3x@pScbS?83{1OPD~N0I;l literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/120.png b/simulator/worlds/sim_markers/120.png new file mode 100644 index 0000000000000000000000000000000000000000..8484786afd0d9ab4dbf61352c568a635535f8d0e GIT binary patch literal 3264 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyyQhm|NX4AD*BG;o81OJ0 z-1XyseA)c~4x^LjY8;CCqPLvm5BOSL&Gm^-_B5WiP!5h^5KtRM zq=dk>eEkEl&%ZJGM{O6ckQZlY;T%Oohrrw0+yNWXj?I~<>D)i_A=7ABc!NX4AD*DiJ*G2n4H z`1#?#=x?C~^4#4%pVFiz%sJrv`VT|s{_Wxn?*th*ghvr^A#mF``PvzYie6)*qpS>x zOrwaD5cvI#iN7RI|AS3-+L<%+C9BTxh%>Zsjv}H%;O%YhnyWka9{9XwqNcNb+B3uT zkC++^7)KEaA+Vvgnvv_;Th<@#+zf|UMiJ2=@a-+DT*lcROZ{0!Urzp*F&cU#B_3et z?aS)*u~ZIj+w*GjS(|5}F$@A~qllCc*p{#V!2wuzR9>7CUb|`G@iPUC4l@`=5wRg~ zW8Yqe|B>6pE9B~F9w5Xpels^|i3yhPe(ewHoOrtW KxvXC)lN;&x2i7ATywVRg986O@ELz0pZC;+3ji~qQ^bMJxnxT@39F~p_X(d-Qm zfo=KvAK2&4G|Wypbs;(JOwFd{I~WvvMiJ2=kaIuoL2!D?DXE!8UuG<~`TF~O0i(kV zhEYUp2;A7W_rP*s8E9;j`ohq-X4Cysqop88r65o-xF!J>gw<(wH>GBNx|Wyjc;tVaEqllCcSX*4p{6K0n+(=3`e;IyHkzxOGuGJaTNAYy^b6Mw< G&;$V8@FmIs literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/123.png b/simulator/worlds/sim_markers/123.png new file mode 100644 index 0000000000000000000000000000000000000000..597e62c3c2113aa99a47d0af6cdb976656a39ad4 GIT binary patch literal 3259 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwylc$SgNX4AD*AC_$G2mf1 zs1fjAS6lRn;DH52_hzzr%rD?x_n9@M_I@104tE9xpHW0y2;`jai8M}rV0!L^h&V$F z=O`j21m51}4k$=Fwq~Yb{v)Oa1IAHAYzSP#Z<0 zguu3Z{SPZjs~gql-sY~E7rysD&u9)NsQ?3JUz=BV;vRfnGf`9S+5RF%hZzi`h}aOg zv2QQK{z&EEb0?q`=fI)a*@wF3EjEz{?b`V!zk2Y)|A&@h7rlE51 z1KYHJGxZoaghvr6ArSri8`}ZX(QqRv+5BdFUn<6}u!eOCs8{0Y>gTe~DWM4ffr~+Y literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/124.png b/simulator/worlds/sim_markers/124.png new file mode 100644 index 0000000000000000000000000000000000000000..98564d65ec71565b30e5287fe8bdf717e29b505d GIT binary patch literal 3253 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyji-xaNX4AD*AC_$F%V#J zyqxe)^RnQNM281a*F!lAmbfQh|H-s!-*R?_Vitx&qlm~5_%=8F+T8R9$#b;V?_f~y z8AYUoK#o6u#X@7-2cFqg42xqJ1k^?m(IK#HIlIAYAO4C*e6C&C|W!SDgpAMtecb6Mw<&;$U0tk-1# literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/125.png b/simulator/worlds/sim_markers/125.png new file mode 100644 index 0000000000000000000000000000000000000000..89e07f9fe56159c3d3dc4e5624c3fb19630fca4b GIT binary patch literal 3259 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwylc$SgNX4AD*Dm@VFyL`; zY})kyR3hUKhgC^0c1IuZ)3&Xg%fH}NaTQ}iAA`e;QAAt_+}LNFc2|;lec?KvLdMZx zBP|4O%!_*v`25tA($rJC3Y_LLkRq|A7CwshY1{gMZkHGqiAyBBDd!?Mqg<(q-ZaSLXiTzs_gp5mtsorcp$6 z2vk=!)@S(Ye^_F?mtk=XgMiv7A|(X2Ef-I?w#(>g%*>*fhU*`V)*Yl&ov^y2=6wIp s)uU;T)U3Dd+!wY35~I}xNwsAyqwiv2q4-;CN-6KS0M!0_A&5pjkV z&QU~42)w<`{o$14%uQ~~KE&@NF1$w*CL{!M?#DHRUwg}H_u@|6gUR#080ax*2s3bu zBH}~f_IB|fD~yd&dwnE-&5(57-?M!*4U&|-fNAi-;%g_S7#lqel>fXU{+ov0XbL1H z1VAa!DLd`VOv%qG#`|+#{7v66S_+aF0-!|5J$I%dFc>HE>^(DiGz3Y`!`y3u^>EYl w%Guluhge1tDIsue=U&Ew-qCO)DcSsExN0ZHU%7d57pQ;Y>FVdQ&MBb@0N`~f2mk;8 literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/127.png b/simulator/worlds/sim_markers/127.png new file mode 100644 index 0000000000000000000000000000000000000000..dd211ad6dcf6daf92a30acedf19fd56ed6de4488 GIT binary patch literal 3262 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwytEY=&NX4AD*AC`Bao}M% zIJ^0if2B-gJsaoB)j|qeH)njfQ}1x~|6A4uTPB9YQAA`2oIf+e*qF`kai32iqr(h_ zQA9!r+}OPL0QcRutUr$4jC)WU!yuqGiii$@Z#nuOmcRSP^uE|_8ISFmGc#t+&*|W1 zIK(oFNC|;?Z&~ABoaxzVoV+jPMe4pItfOTgDIswF8&mx&U`n(ruWsCL^4W0yujxA( z6nsVz(IM~#SoXEP1JxpJCgKb&oTG@85U{Q{5H775euIU^t07)wUfu-M% yvp3@!7)DbbsTptX?!Al~j*V6qM7EP5%Ixz#itwL!{A(?!W8&%R=d#Wzp$PyE9f0}( literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/128.png b/simulator/worlds/sim_markers/128.png new file mode 100644 index 0000000000000000000000000000000000000000..d59304d157b1b04156cda0830a6398b90d5292de GIT binary patch literal 3263 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyo2QFoNX4AD*BU#YIPfqW zoPGG`avS->jT0-%LzymB+;n^Xk0EsbZEl8Y7KTHkh{zC_H`6dZji=_A?94|@4F-&( zh=dT>kY3#wes`i~ddjH@{zJLAC_(2d*J_Z_20&$K}b>x0*7CZ&rapwwlg+q&)Ofpk6{o{8%0Eiz_%Rz z1M4>LWmr8LZlop~V93+J w#<4^vSyJO^xT-IE_l@m<)M$l4QibuCnblc>+q68y1k^L}boFyt=akR{06fou3jhEB literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/129.png b/simulator/worlds/sim_markers/129.png new file mode 100644 index 0000000000000000000000000000000000000000..9ead7fa9ca59bbdede894d2dd73f4a8a3196cb30 GIT binary patch literal 3257 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwygQtsQNX4AD*DhwV7z(gD zp4Rw3A-&t0MM5a+hbO=;e9awzsON07a?e%wlo_tqxy1e#`tR8VC=4jOd z34z;}ndP=FW4C$f!>@2)H2sm9{eVf3d+tj{|ESB%^S*@27q@dW9AX(oM2En)FAd_r tuzR36nr%rfk)qS97!8h%W?7Q5Y(2vwVS#15GkrmQ5>Hn@mvv4FO#mmRHJJba literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/13.png b/simulator/worlds/sim_markers/13.png new file mode 100644 index 0000000000000000000000000000000000000000..be27c394242d371b8e87b62581586fac2c33342f GIT binary patch literal 3265 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyho_5UNX4AD*DhurFyL`G zxU1s-)VrLW1fZ*Q0)As6+WeZ{Z6dl?G)860MeBH}{e#yrW*X~zzv&zY#H$G{;x zibx58+uOw}78)C;o$dKKY3B5wR}SxBQ1BT=M2A4m{kR8#&%ZIfH~xJ3+KDME4dWj% zH5f3CA`(JiLv3}Vc$9JSxid5LXZ}oees6!6l_8O76cHT)zrQi{M*(y4rRjFh?!-0J zk7i<$$}n&yPER={HS?3E^ZuM?f9;QsCO}g2A2HfYNX4AD*AC_$G2mf1 zs1fjASDRO5KeK3cx|D*VH21pCtRc1c;}~|hGbs3sBH}_I=X_73aqK+?MS*b*5eN)2jFHnHmfjM-j0hu%Wh^@n4bKvNhtql2x0+3mF|| zFpMG+Lg2=}y$6`*&NR$U5(-2lH#0v?+7bHBGV`$ zIs|@yW8yDKJGN$~p|Z`*`lq8MABhD&u<&c_kJ>K&pxjyhS&vWU)%a(lVMt0Cf@+T+ wGfJx)pO01?q}H1^thSpotQ!qClEUomdKI;Vst08@&?fdBvi literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/131.png b/simulator/worlds/sim_markers/131.png new file mode 100644 index 0000000000000000000000000000000000000000..75d111e8010e9631528e5a6a0e66527f8267348e GIT binary patch literal 3254 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyt*47)NX4AD*BG;o81OJ0 z-1X#teA)c~gKJOAo-t1nj@a^zKj5qNUIqhq1_hr{L|h2G*=d}1SCTnCIez9NrUnDX zQA9!rY)G$Sfjj+qUsP9Zh^B<~?BI zYg~M7swOZD*H@jHyZz2D00R)z4*}Q$iB}D!^{0 literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/132.png b/simulator/worlds/sim_markers/132.png new file mode 100644 index 0000000000000000000000000000000000000000..a6954bee25286a7aeeb160b10761835085b77e17 GIT binary patch literal 3265 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyho_5UNX4AD*DhwV8uBn4 zwAk@~+FY(rf)-Eq+@8s%a{u6>(tQo9-rweC_|C!5!a0hF3xT&Y4YSjD_8gO(`G~2( zfN>O&5CR)&s~g?tPSng!IW=LX|2M7mzy9kna0rhgqC?>JcJYcUQZtL(mhn_xRi9mX zX7vsR1)otwN(kiKk9)wpcBbKT*We%PV;BU~MiJ2=uq|Kz!wO@g)Lx&NQqHru84j_G zBBDd!+gsKjXLs&B(0uL8jG6vsUuH;-mVTrb{zwI&ZJU`madq}+;|3A}Z*OzgEGVsJ xtb2I=&&<)xOKRD5ZRcLbg5J?^BPrbK7%naq6?Eu&!Vc=2c)I$ztaD0e0swi5TP^?q literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/133.png b/simulator/worlds/sim_markers/133.png new file mode 100644 index 0000000000000000000000000000000000000000..1c3d619857d81bd01d90c6f6b053b832d0eb53a5 GIT binary patch literal 3260 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyv!{z=NX4AD*KX$?G2mf1 zs1fjAS6lSS;RZ+9ymTprzXzIL|6vH-zg?W+ogf2;@F*fK1a2EAUppi5Ln=L`i<{vP z%P1lx1irmxjVsO5|FF!|=%{vfF{8r_hEYUp2;8tTPIg=NVP)FCO^VL;pU+s2h99ZP z=f=Lh4F4^agRh;Kq8R_xz&M6MKy4He9Rl0(^*^jKHcIXFk(4t2eCkJt9&u&nXypP4 zf!o`~E3Qh-EOJ|RfI*z0g>w{<5&~~;bMJZ8;}fYITygGu{TWtr|CmtP$F#R@FQ;&f| zcodNm0@cgJEwcRe3oh+4dK&6qc6_dUaVIy!A(l}@bO_9Q$=VlnnS0Kb<>D4EF7;Pl zo*es#slkA86p;`D8`7&9`M139i7Y?1r&Im6xj1oof3%QiA! literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/135.png b/simulator/worlds/sim_markers/135.png new file mode 100644 index 0000000000000000000000000000000000000000..672b70f1a11c508d19b3b9de74b8508d899b0fea GIT binary patch literal 3254 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyt*47)NX4AD*BWyh40sq0 zE=&49^|GXvBKwcstT{?wKHai+xcdGjYeO*$!=X_`WC*OAX_%eHQ*%si<|C#C1IAHA zLI`Yl{e?+?OZu@jGYyq(ZU*mUQ1BT=M2El|tGx%-emgP6)aYrn{9)(O&?7bRL_5FSNDhrn(>{SQvRzc3Yui!-!vjv}H%Aa=Rzq`)9JLtbe$$_&-Bv{dRGNZyXFQoTG@i5U@2)K6ggqM~}79QC5aT zrcp#n2%P`Mq+haK{D*IPHDlOL1_hr{M05zexf%DM_1u{mzZYsc`ycBc4Lwp54>$nd zePeoW{3Rtl?acHYLoHVsSlpx_I?`zopr0AM(^djJ3c literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/137.png b/simulator/worlds/sim_markers/137.png new file mode 100644 index 0000000000000000000000000000000000000000..93bcf70140368698b1a400a3c8646e5ff52536b7 GIT binary patch literal 3271 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwypQnpsNX4AD*AC_$G2mf1 zs1fjAS6lQ+dO-QTnQR_64>Z00!w|ZEyEwx;K?V-tQAAt_+%`_Wc1EJ2*VyPND?=jF zC?X{Uet%<%e{rTK(m2`eN6P$DqX9@#`T>Ssztwi}2difO-|x1Jr;yQM2E!;KAp~yh z+k0T~wG&f}jZ#mnmNWkRKYlb1lU9U*GH}M(9!urmG+_8m3$NXzs7GA6IaNN8v+|@ zs~g?d-sZM>8QTBQc_%61KAKE{A&@iowlu@_(M(HHVtB(ZbxDHnOYDJ*pdN~+tDnm{ Hr-UW|?KTi^ literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/138.png b/simulator/worlds/sim_markers/138.png new file mode 100644 index 0000000000000000000000000000000000000000..a1a16b4053dcff45033292d81f0588123cbae874 GIT binary patch literal 3269 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwykEe@cNX4AD*BG;o81OJ0 z-1X#teA)c~gHN$#&zL6(M{N1VAMmxhn(=`gV}rpcA}$0rq^F&kCCR*gqx-TQ3<^G@ zh?Ed`vvV(l%x3pxYbI(s$1n(}jUu8$;M<+J2PZsjv}H%z;?U%52xSXnC_>a?V0)0*tvh^L#ENX zgM<*+@cbK-``d|{*(s+kB*%R{`NMoP1(KQpfr-!}>u%hGB{Pq|nW$N#H5!5>g&;8a z9;_>^Zk!zVb%x|kA@J=jt6ZtuvNaPmo#j7kIqQE`(_`Qe9z{fl!0qkg6<4HY7P&3kbL!0D50>H# zEu5o>ln{7(o7?79k58m>a9rxM#64%8#}JqAM+-xgYl+@cB0;|5-n0m>Q+NNKCVP7CM@X zNh+Ygxwy2NvF>@|Kkd;^R;A>jZ3 literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/14.png b/simulator/worlds/sim_markers/14.png new file mode 100644 index 0000000000000000000000000000000000000000..d836c56ed92d6599c67d868a82bbb90d23ef02a7 GIT binary patch literal 3266 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyr>Bc!NX4AD*SdRC6nPj9 zK6d%1x!Kc3k~em0!`YgsxaaZ#U;lq&I$+DhkT{Bn41wyjGqWU_&s!)5$1n(}jUrM) zU|YWahvsujX9%&a_fCVJViJx5qa%Rf>=0GJ5vGw#MQ?0dx2V8A$vhz)@a zwbhOOYj1Pcyu1_lz*&Fq)3xz4xfu?zj3QD(;M-ePyHdAhYbI(s&#O9f=KKHAGLWpF literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/140.png b/simulator/worlds/sim_markers/140.png new file mode 100644 index 0000000000000000000000000000000000000000..3623c012a2e2f1de47e62689c2cd83618eeefebf GIT binary patch literal 3257 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwygQtsQNX4AD*DiJ{JMuUj ze4YHsf2VoV-v|knH{nwot|b4tu=9Vzs{gmS8LC+r4viurLtx%a!}K(snqF(8qpS>x zOrwaD5IFyhN&ZEi{)c78MyW3*pS5|mdIy7o&nO}~1m4_?Yq&pWqGox@sR=WWpZR0p z9K#@>Hi}3Ifp0nb4C^g#bJxg;GqiAyBBDdU_BQvPSA9N~%E4`N#>p=xe@xe7;1C`~ zM2A52Ht~e(k~53km+{Q}nKGJnNiMlYvo4th)?91jv=pwI{+ad5xJMg%B!({(w^QABhIFVdQ&MBb@0A?|pX#fBK literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/142.png b/simulator/worlds/sim_markers/142.png new file mode 100644 index 0000000000000000000000000000000000000000..7b808b518f82a1267218995ac58e49f51f4f8e9d GIT binary patch literal 3275 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcyIBu^K|kcv5PuN}-iV!*?2 zP@~|#uD0kCK?i~2do$TQe)4gz`^y?q>#xsnMv#F+coY#A0@d$&BFm2*$bO?89K#@> zHi}3Ifp0$g2lBstVPaqVlJ!R`H^U*8QABhI%zMe&ck43uoGt5oDzB>V{yf9l{*0;~ zajAARd&5JZdYQOIR_x48>wR{bJUgfUcz+S2!wiN|L~ID$Fy4D0u)3=8eDU+iYqf*( zdMky+8Cp0;5z!%Fdzo8iD=-yi`s){5n0@cly84-;MIos*0kAZ*yRgqVEp}$nOT$0G zqvasHfFp(hEChczWLGs#j{7R{?+hzLBGV`$B?Pkl^BH;yM=K6e>dhPccAv$0m3A%I Q3+knKy85}Sb4q9e0Q0s$H2?qr literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/143.png b/simulator/worlds/sim_markers/143.png new file mode 100644 index 0000000000000000000000000000000000000000..222915575dc1825f147ed4583bfe0c6ddd9ffea7 GIT binary patch literal 3263 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyo2QFoNX4AD*AC_$cHm(+ z7_;DCbePr&rrnhQNmGYDTVe-%5YFpUSI~_jYkJ9AX(oq=djZTjR8pQxj&YS6`p~G2OV{c(e{CsR;tC zfNK_P-pgQfbhQ2;wfcO1?;bHfYNX4AD*BWyV8}Kk3 zTy^1}_G)7hgB3^Ww=-z6R~F2bU-0VtH>Lx&Obm&mh{zDAPCGM8l6ifh+p--D3O=KV zln}_dANL?IJ>`_t%unl%pZRd|-&xktlt)Sk{Qkz&AEg|8?u3ZM1EvN8#!*B<2yCdW zZq(nBc5Kc}!zDXONw%Z89Gr2Ve`C6z{%rEviJHO5_l}I_Tp}wBh%$K2ouX(wzsLXS zXznGk@B-yt_qh`_vr|r8NKWI~oBCpUrLZ_d3+E^zIt11l8>gjk?KytN{_<${B`Ny? m6~Df7k738L(QqRv-0JsTm*8BzutpNpFY$Esb6Mw<&;$TOuR&4( literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/145.png b/simulator/worlds/sim_markers/145.png new file mode 100644 index 0000000000000000000000000000000000000000..fdb0828eacbd385e6b0b2fc5db8dab3cfa73ae5d GIT binary patch literal 3268 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyx2KC^NX4AD*DiJ*G2n4H z`1#?#=x?C~^4yMTpVOo!%sJrv`VT|s{_Wxn?*th*ghvr^A#mF``PvzYie6)*qpS>x zOrwaD5cvI#DgMQoo=D~3Ho4Dg#`b9)+zf|UMiJ2=@a-+@z6-M?i`|y-*rpvjqh2j4 z&d|a+ibx58x3{@J%r`Z9+T$~`l6!tJqr(h_QABJA+}OAGz~XZ!rkEO~zA!XyyJz$z z{oaw$f{>(o09Xq4M=1vbLs8K#a`tEmNNNQDECl6B-IlGHsOem@$!xR$B&qxdmVP!_ zcjF%X{*jWaM_iLiWKj3S)78&qol`;+ E0KJJdMgRZ+ literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/146.png b/simulator/worlds/sim_markers/146.png new file mode 100644 index 0000000000000000000000000000000000000000..a0c4787b6a5db37f2e0940e29cb10937ad170245 GIT binary patch literal 3259 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwylc$SgNX4AD*AC_$G2mf1 zs8R4=S6lSX;ReU2dyIJ}7(HOU{*Q50oxeWA9AyRpwNXS|2$Y@ei8M}rVEXNZh&V$F z=O`j21Y(zqKbUN5l-lbf`DpUpXG{$SjH8Iy5ZLhg3se6s<=|^4rmQr~9}Pb;(hevH z<1^;RJ@Cv<>GJ;;zmq}1XA}_~0&lGL9$0+sCF{H`{`w9L+zf|UMiJ2=uOXSCfy oYO`%^Z58taiP3N)Dct@t8pMcmzV4g{>YFfly85}Sb4q9e0GnNt?f?J) literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/147.png b/simulator/worlds/sim_markers/147.png new file mode 100644 index 0000000000000000000000000000000000000000..452915a91643107850bad3841e63678dc41db394 GIT binary patch literal 3270 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyucwP+NX4AD*ADhRG2mf1 zc-isi@-@8)a@;&Cw(izqykvCYfz|(pRsXk(GrSXI;1C`~#D&0ZR2*w`FT4Y8u}+`eL~L5mSQ!<0v9F1UA%GH?mu87k|(^(|)ttGM++4 zhZzi`h=dThv2QO!`P|#wHJ5koJ@9!4gM!Z}B02HBD{8Dim_2@#+lXgdg6;m3qexq z0ALAtfO+k0ZkvAf->YL71k^?mDIu`!+*{s;?9p%|DcO8u{#qi=`=MD|5Y$2OboFyt I=akR{01WC`-v9sr literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/148.png b/simulator/worlds/sim_markers/148.png new file mode 100644 index 0000000000000000000000000000000000000000..d07d02224b02bb15b302039a73cc02121a214216 GIT binary patch literal 3261 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyi>HfYNX4AD*AC_~8}cw5 z)Cl;mt1bFN@PWdi?Z&(l)TO!CeP#~%>!;6fMv#F+coY#A0=LVLt+7siV0vzIJ*VI9LDpf-w# z4uNgU#VfANjB5zYu4;V$n5n^laTJjd0vl}i9uSWL)*vtUWSrVvH*+-kk(T?w$!}KN ygWnaK%tlK-QVagI#(NnJjpb7S2&bTnN0KX_%eHv*(!P%tuTO z28^SKgb>(JTixhBccNx?%Bj_MY0skDc>eyE)MMZf9z{fl!0qkg56po9C^_>`{L|4C zNMZ&Ar@*zhxouwd_*7n<{LxUHxLiM4JVHX?EimX__4q_82e%zNV{>!$Xa**^@B(Jw ziYvSJ9{6wkIVC-%>;Het(KJYE_CiXA6`RaP!;hr!0|j2z-M9z;Kc?i4R=%XRJ))m~ iV>@6v8g3+o+i%8@MWVvuj&~P;Iw+p5elF{r5}E*8)d0x= literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/15.png b/simulator/worlds/sim_markers/15.png new file mode 100644 index 0000000000000000000000000000000000000000..016e11fced84c359dc54efb80261453cbc4828b1 GIT binary patch literal 3257 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwygQtsQNX4AD*AC_~8}cw5 z)Cl;mt1bE?@qvToy_swtcNw|YeP#~%doPY*hdYCU&nO};1ai*zL>ebQ$euG%Q;&f| zcodNm0=Kt||7bsVW`?Oz>N7**{D-xpK}b>x0)}7370H>K+?E|+5NBxN97Uvrz}ws0 zHd(zsmde3xXKIY>rgAeJVi`q5hrqYDtQD!?;7hxA=ywc*fZ8Y`Is~@m>wj2bYy=Fy zpIXND$L3egA5DFvWg!ndw1g=c*cK~u$wZP?nq5{*LLn@Ea)8# dH|$vR<8g6 literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/150.png b/simulator/worlds/sim_markers/150.png new file mode 100644 index 0000000000000000000000000000000000000000..50a3c73da21ca582cab46c1cb0ef37cf83453be3 GIT binary patch literal 3262 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwytEY=&NX4AD*DhwV81gtA zwAk@~+FZ6t{mn;D-JPB35T$Rk<(zoHSKGY|2JQ?BKBI`Z5XiYNx%pjBV}Ic~pF&25 z84RO{gb=tfFRo$#oR_RW?#_yP;Hk&JAv}tR4uRX3x%VtAPCv8D_!*b~xA>jJrPtAn z4G)1FfBg?DjQ1Xx{I2HAE~BSH{@;wm8Cp0;5z!&=_9d&_)@9-q_RmjE@wR_j()Nd8OY>jqs#oYh@M{_S}g%>>c&RcVq tl_8O76p<1FuYK|vdPbXkB(->N@c&sT#`n@QvKrJe@pScbS?83{1OOANimLzs literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/151.png b/simulator/worlds/sim_markers/151.png new file mode 100644 index 0000000000000000000000000000000000000000..2d17931a78785725af2a413f82b40e42e0c0af6f GIT binary patch literal 3260 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyv!{z=NX4AD*DhwV8uBn4 zwAk@~+FY(rf)-Eq+@8s{w`U7Xoi*8fK^Q>^UYm^AS^n z0plnlAp|zmRyVrOov4|e!o^d-=rDs}6cHN&H}>si*l)RA{D;l!Z%p%#voa(yjUrM) z;P*GCJ6rSgKP)jedK&8gY&7qZQgnec?%Ii(=_#jH+oe5={$VXnTxmI4tw2KH?QQOw z1*XQ&&h%7HUTFVWO^<;?codNm0=Kt|S6q>txyfxAPw&i1qkr0?DUhVh1x$f9S$E?e xe6HAJHX3v!1szcF+Tv>F2U4TqMpC%_WmqUCETrgsUIx@H@pScbS?83{1OR(iiS_^h literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/152.png b/simulator/worlds/sim_markers/152.png new file mode 100644 index 0000000000000000000000000000000000000000..23d8aa1de31278649beae67df6b1593c48efef5f GIT binary patch literal 3257 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwygQtsQNX4AD*BUz?81OJ0 z{66_#^tY=80xEjemm5Ub#peFzU-4`IUWN_s3<^G@h`11VGgGoS?bw0zI}1y-hsfy6ns%w`CuyM?;UK!~+bvAI=eam{UWSh9cfzgylQpN)&yY;|q`|iXP|Cv7x z|IFiNIK(oFNC|;?Z&~9?-IvXosOg+G?@h4X*Z;8$0&1g(=n(jpqyJ&m=Di2*KR$UY whG<=*Aq)wDvUlIu4oHbJv~Z3hqC?;>!`ujA0oTVp=Ab@_r>mdKI;Vst0BT+5KmY&$ literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/153.png b/simulator/worlds/sim_markers/153.png new file mode 100644 index 0000000000000000000000000000000000000000..53c1cdb95a6f2f7fa28274505eecf2bcd2d43c9a GIT binary patch literal 3259 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwylc$SgNX4AD*ADXDG2mf1 zs1fjA*P8c99lOx;xtR@HSamAq**jdl|CY7EmWd&86cHH${rfM@$U{jH8Iy5ZJJLF9U0OHKSd@XwgSX$qy{|9FVdQ&MBb@0FP!=-~a#s literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/154.png b/simulator/worlds/sim_markers/154.png new file mode 100644 index 0000000000000000000000000000000000000000..e1744610b6b66e4d22e4c1edbb6fe9ae7fb35df4 GIT binary patch literal 3264 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyyQhm|NX4AD*ADjHao}Na z>|gjNSWf-Hc}AwnfN)g@uUokv?$|qAz5kZA!Ip_3aTF050_V@nFg9khd)((!$mlSG zVHA-N0yj4AJ+S)Si7CcLPow#b4{QH66lZAR97RNjz+7wNv=pwI<7ezIvySFj5<}qp zH>UNwZgcN>nWO(<(L(;uOT*2MjD{kS#U4Z%CoIT$k0Me+pn98l!u7jx4WXk!M^efGhFiten{f|(=T}M8X^vLB pq_#We?%vC|;n-+-M^btBpF#M)IQJ@*&C@_#6Hiw^mvv4FO#ppC0kHr8 literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/155.png b/simulator/worlds/sim_markers/155.png new file mode 100644 index 0000000000000000000000000000000000000000..bd8357437fd018d23fc280ed4ad3efda83662bf8 GIT binary patch literal 3266 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyr>Bc!NX4AD*DhwV7z(gD zp3eEFd0OztV~30#Z&$H=c_%h2@;Cd6UvuRc&ImGa2#+G-LZJGc&E|JD%)pNYOnS*x>L3hTSD#7+zS)|9RQ| zH$vhJEu5o>=n$~I%)IZyJh=n**S<8oe>_@`kXVg^>yhfJ1D@Gc42*ijRotU397qUM zFJqV4x{UomdEscyOJc+0#;wcT4ThuXjijvfo4L_Wl(+xJwdtVFiKnZd%Q~loCIHe{ BPHg}H literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/156.png b/simulator/worlds/sim_markers/156.png new file mode 100644 index 0000000000000000000000000000000000000000..5565ad0e21f99f94bae59137f69feea8064c0ee3 GIT binary patch literal 3261 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyi>HfYNX4AD*DmHhao}M+ zIJ^0if90G8dsd;aa8(2T)vnFY|1q!nm(S1ejf0_ua}*I50=CBI=gdrhVEpccwjKkA z@F*fB1gf{O2NY~)-}5quzhW{s!y%SYM05zud&{_fm-6~K6T{oi{7G@P`?VZa=`=T#nm#pRoF4h^F@n4|*i+}z5v zCsP%T?Qc%-ubt^X^XKfHq@>-^tPTtTUAgYH;d)c+PE1Gv5X?3L*Uz6){3V+K9S17KUR;XKT@+F zIQ^AYH~xR#<0JXYVBi1SqhUx=7=i+jf9_1f?37by4)g5YBxx+4)-jreNh-mBSy-;r zZP}WMn$G;j$uFjUG!tiN;T%PzguvU|+%{Qv;~MVI1u|YP{%15Age0XPVEBDFwQDZ} vqaFi?@F*fB1frjRV>@6v8g3*do8OG3?P3Ct4WAta^-w%r{an^LB{Ts5?(!M% literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/158.png b/simulator/worlds/sim_markers/158.png new file mode 100644 index 0000000000000000000000000000000000000000..30cf7c4d8d951e1535f1487b387bd79cd739267c GIT binary patch literal 3264 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyyQhm|NX4AD*DhwV81gtA zwAk@~+FY)8${Ql??3>A^l2ef}SAM~(@+!uJJ_d&wqlmZ=xUtVT?XD#A`oeWSg^Z)Y zMp_8mm>2hed+tltd0Uo=S6rGqzw+wjoeT;-qloAb$nn?zu)=unf$twva`hNEghvr6 zA#nRLw@p^;%%XKZ5C6aW^MCaYqQiYOjRHdel=xb&ePOC8Sm#rD_5beC&?6-TfT4Fl zJ-w=teeFxu9|u?&5}8I3DIri@)#yG~JNVjE&FRPPRBrOt`+H{gXa**!U;@`3rlYk7 rdDUigdKGg*4>!XhmQh4>2>fGExFaGEC&DfW>Y8}E`njxgN@xNA6iGv2 literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/159.png b/simulator/worlds/sim_markers/159.png new file mode 100644 index 0000000000000000000000000000000000000000..27da7fb1a23c82e3a803c82c68678db88e622dc7 GIT binary patch literal 3250 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyrKgKyNX4AD*ADu!8uBMwXzUB#Ht$KWty6cHB!H})B)-IZisU%1Yvka0BF zNDF}*^Wq-JTV3Y1$@JAfpdrrC!a0hF4uQ8XS$~|J71zKp8g8T}8(`4Mm9F!N3||)W z==7}^1_8BEM05yjTP|L4WtY*@n3+W{4fm}%%gT_*G>V80f$FM8_qi`w=Y3gq{KLZ0 zq(^GbL!`b>>qY~RjL`ehe(ejBf7R9GAI75<2dNduTI0Qp2FFHAJdz50dueSEfpzlB Q-h(zopr08>jlF#rGn literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/16.png b/simulator/worlds/sim_markers/16.png new file mode 100644 index 0000000000000000000000000000000000000000..150574dee30a9b26fc9ade423131a7552fe0c0f3 GIT binary patch literal 3262 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwytEY=&NX4AD*DmHhHQ-@5 zs8R4=_qEoJeg?Lgd*)^~OySk3cqbn4b?;t=4eksIKBI`Z5O_0FvN-M7f#W-rgJT#3 z)J730A@D6n{{#EonTF{pr!FL?ovGQ>|A?uNN8v+~Bs~P`2@AI)#4sQFAV*hM3 z^hiuRpa3kp`<6B0>1fcA5_G_DYYYd5+r1LEWx#wpb9>FT|MMS?h9SveICr9Ec?y@k z`RD)kUEBkAu#VPYh3Ae@rui5lFywGsnZr`XyF`1M2CRwZSFmpu*k`?rfTyc1;L5FSOug}`m&7R4L6d)t=`E?jPL59*^faz6Hiw^mvv4FO#tkcP(%O# literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/161.png b/simulator/worlds/sim_markers/161.png new file mode 100644 index 0000000000000000000000000000000000000000..37da960c0361878307e1a6caf714b711d910a495 GIT binary patch literal 3252 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwywWo_?NX4AD*DiJ*G2n4H z`1R4h=x?C~^6ZXwpVFiz_?&QlUDL4Y{aerpx zgusT{>PGjq6E%UsxLfY9{nOC^BqjZTLvQXx&Fqv@tL2P8i~T#t%8gTe~DWM4f!SQ={ literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/162.png b/simulator/worlds/sim_markers/162.png new file mode 100644 index 0000000000000000000000000000000000000000..241170325921bd444979270c56d6a63624a0d7e5 GIT binary patch literal 3269 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwykEe@cNX4AD*BUbq81OJ0 z+*R>^>Sal;V@f}Fvo1OEA;Z?*;p+P@Ob28b8w^GfaUrmwHvP;lV>Y|TJB^OAG9)sM zB2q%2x~g&UJni6XQ#GgCJ?r!TyUAO=xSgBf5X&ecIt0FbVJbK?EAByU41<8$C?X{U zwk;R8$cml$DQ0HT%VfK!=T6tWN*oPEk`fUp2w%+vroMN7HkGDw{eR9hT6d5T0vl}i z9tg~?YMeapCF{NC!SCxwOFxnee_#RVZhM(~&v{@WxWxMX|CRC2MiU`%trti$51I%W z-75dr7c)A{U>HRtgusoeW#$aWb}%UTj3S~#;0^!zZc#zotv{PU{S!}DKbLh*2~7YN CE38Za literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/163.png b/simulator/worlds/sim_markers/163.png new file mode 100644 index 0000000000000000000000000000000000000000..aa9761dfd24199cf4e9018a15948bc954184e1e4 GIT binary patch literal 3274 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcyIL{AsTkcv5PuN}-iV!*?2 zP@~|#uD0lt!Uo5=dyRP~Y-(h^{*Q50oxeWA9AyRpwNXS|2$Y@ei8M}rVEXNZh&V$F z=O`j21Y(zq@3?BW_rTn1FInfMKl{J`)$tt+3O=KV=n!~gwfDfVzLPaK?m;E69=3lLy_4ty zbhNSoh5)b@VfY`pT)aYV{+nby1`gp-L`n!m|Ng>uz;HC&NJ=)pnP29J3MITK*$C>S Nc)I$ztaD0e0szG51(^T< literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/164.png b/simulator/worlds/sim_markers/164.png new file mode 100644 index 0000000000000000000000000000000000000000..c3cd2529f06cb0aa07d4fe7bc262819ac085a9a7 GIT binary patch literal 3262 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwytEY=&NX4AD*DiJ*G2n4H z_<7>L=x?S4zXdsrW}9DbNO6*j`pv%Lm+fALf<6X^8Ka1}5V&D#oOZUS@%jzr;1~u0 zwNXS$2$ao@dk|Qia!P9Eq1t`_bM+WFghvt4A+Xy||HF#Xs>b)5-IwvyO5Fc%CeF~p zIf_ULf!O8Z6<6%`9{3))T>OE?X!wzoe1L)XV6v%EYQN9S%G0~$j1RLiBr=U6qC?>N zOIEq9e)Ijigd_!F_W2&knN05uKd0|v sQ1BT=q=Z1u+so1n$9IsTWi*7}@LQ&cbF0J%%YZs2p00i_>zopr0G7TGdjJ3c literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/165.png b/simulator/worlds/sim_markers/165.png new file mode 100644 index 0000000000000000000000000000000000000000..72bab1d0bcc74751d0e964bd74335a8f9721948c GIT binary patch literal 3271 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwypQnpsNX4AD*BUux40#w1 zTEzU)2V#E>|Ohzx=IXJ(ihv(4M+zHA4Bg3l-- zB?R8=-g|)mmT~g6Gc%^2shPRlu7{i95X&ecIt2FJ=9by&zHH4zP3L)4o0ji8!z0en z!a0ga34z+};ucxGJ~x$v=Y6^|`Rt$5ZKD~Pq;d?DivxduW7?m7H?CoI41<8$C?Yxp zzTJ&`u%fiOaq>6k;5M7Df73@3A4!Q1s2G<1)YnebH1;?99Q~tSkAXvY6cHT)zw`79 zuI$=-;Qhyx+|i&THRVMA{>FB|bTr&Z3b)_vk1fRn8nun@gL){Qu6{1-oD!Ml)B_4Z_qh`_ zvr|s3K4a4}{pTERhC?i)i0Bab_Lfy6V!QYQc|8UW;Za0%2;AN-UU5ZgW|7-6p4u}# z3+Lw?n=N;Yl_8ahA#oHD9|FI>G4)$*7yn^rY=2W~=A*y%&qr$zQYz35rD?}{d>X&c y_?f8g8V7z#D#%6fv%Qe`9Ju-4ah%KbLh*2~7Z0^=p{` literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/167.png b/simulator/worlds/sim_markers/167.png new file mode 100644 index 0000000000000000000000000000000000000000..e20aaa7d7d3626705cafa5da01d0f9fd88fa8136 GIT binary patch literal 3260 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyv!{z=NX4AD*Dhurao}+{ zm{agy*IM++;{zT)?)flJy8hw9&iaN`_usNM*fKFBjv^vM;QE;v#>Q;>j@cR=WgQJR z5<}qnHzxg(eEkEfHt#(!Ifg+%Z4?n50%f`SA68UXH>%H_sA=qP_F4D-*Z=7|7!-U) z5h)??=2qN;!1QXyu%G|siy0keFpMH%L*T~Ny$rk0otRRca%#d%b?HY;4F-&(h}aO= zkX_v9Au1VGug_1u{mz_fSfc+J(xwW8t-Eu5o>=n#m#&HZ6M yu-g4yv8jJF`H`BZ-rTt@&5%9XVj-!)_Kp3qwXjgC^~@wtx5U%c&t;ucLK6Uz<1{7! literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/168.png b/simulator/worlds/sim_markers/168.png new file mode 100644 index 0000000000000000000000000000000000000000..b5f06d2cdd64091d9c866598566804b4b3fc8f4f GIT binary patch literal 3268 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyx2KC^NX4AD*DhwV8uBn4 zwAk@~+FZ6zc1-QAdFfIUYS_E4{bX9TZ@DzwD_liy~{a@eC&2Wfi6cHT)-@Y)NvAWD{lj*Df;r|W> z1)otwN(kim>wj2LntE!d(bLfWou9@2onvK4WEw?8hd^~zqx;;KtUvCqt~)a;?t!Ns z1BdV^B02Z00!w|ZEyEwx;K?V-tQAAt_+%`_Wc1EJ2*VyPND?=jF zC?X{Uet%<%e{rTKQaQLy?z5V)eOd=M!y%SYM05yzd&??U>b7jnM9ueAXL@G(f78+< zF4>OeZ+HmY-Y#BoW!GMY8gX%k7S2&L8Ui#6fw#B0YZe+Cr=9JQd^ovQYc#!)n%#f_ zX!Gh$+yhVJeqg2XBV@F+gBN4OFknU9|If*GQ%9>>Qd=6=cJ5^?=p79=lEUpDLnpHc U&y9Wezk~WBp00i_>zopr0L~yXV*mgE literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/17.png b/simulator/worlds/sim_markers/17.png new file mode 100644 index 0000000000000000000000000000000000000000..fd85f29ae8916c382f041bf2bdc07b2cefd40dda GIT binary patch literal 3267 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwym#2$kNX4AD*AC_$G2mf1 zs1fjAS6lSXd4b}4Gub?%4>Z00!w|avGB-mp3&WvNL}Unjn``*`oJ2)$acUR$Xt0qO z0^hzc#lP^;|FEPq_0-b%vggThM_CyXnMM)OAy8e_sJ~^I_=D!+)KjbP-5(7-QWFm_ z0Nv+m2Va}2Io3Z=Lm`=?9|28^SK*bvxY zyO&|}wJ%KmR^iL`yqdm~=wKgBqL2^(CBGG>dk;)L_l1dn*B68JRsXGH7zETt5z!&A zZMpb^$)?6WS2zFw literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/170.png b/simulator/worlds/sim_markers/170.png new file mode 100644 index 0000000000000000000000000000000000000000..eb79a5bd5219e80dec558f7b01b4225f6b55e414 GIT binary patch literal 3264 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyyQhm|NX4AD*BUdAIPfqW zj4Al9Yi(*Hq4|Dprh}EV*xcXjD}LRLW4NKrAfPsihzo(Tvptr^$q!85P1Mw5;1C`~ zq=dlkZQ>PI_U=6}`P_*qGyTmzPb;@gYvX1(#4?JA4uN%VS>;N%i6>0`|KCuYp@nl4 zkrD#2x4CN;R;QhrDQSF2{rCD9;xhec=?DpdvRwTSt>?Zm`Bz;{{xM@TOTHRtgusohdk={3NzXA}_~0&n=&yNL)_id%<)x+b2kelF{r5}E)*OqN3c literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/171.png b/simulator/worlds/sim_markers/171.png new file mode 100644 index 0000000000000000000000000000000000000000..6196162c99410fbcdb3da0eed9e7fc501a338248 GIT binary patch literal 3276 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcyIWKS2zkcv5PuU*V!G30SL zXtCq}w7G0W@{P{t-po2^ z{Lpyr%nVbb)Mtjq`7?hR`#)kDt$s-hfep3QjoYo1gRh;KqG;Sd^QYmTxuZ1+Nm&_~ zkMCvOjcd67)A0Y)(Xb;a?0|}|?cB>)&^sD#B!ycYL*OO}K3UOeyFlF(Pgg&ebxsLQ E0MH__r~m)} literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/172.png b/simulator/worlds/sim_markers/172.png new file mode 100644 index 0000000000000000000000000000000000000000..70f25c387396fb420bb62ad008bafefb7c3e1f0c GIT binary patch literal 3253 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyji-xaNX4AD*EZ&|81gVU zo(}l0ds?*S!vV*Z{B$XWr3nX1>l#+wf63Zl$;6O2iiiw>-{)qOrt|DMw$tb+>u9i% z7y{K*j1^~g8l}e0Eb8QDIK(oFhz@~oUzlo&mWfwfm=)K+AHyJ^Hi}3Ifo;ph6RrV+ zE>MqwLwFPs9Rjy6bN_LE4l47`h)&BU mLSU`&UPgmsqiK$$G*@rkC(NhA*ewX^k9fNJxvXpv~LSRFB+L@V>%<+%=dH;J(T literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/174.png b/simulator/worlds/sim_markers/174.png new file mode 100644 index 0000000000000000000000000000000000000000..0931ea7d2fab97bb508ba3372cb08bacd0aba350 GIT binary patch literal 3253 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyji-xaNX4AD*AC_~8}cw5 z)Cl;mt1bFN@PI(=y_swtcNw|YeP#~%>!;6fMv#F+coY#A0=LVLt+7sikUd8`IEFz$ zZ4{9b0^630SFA2hJ+;f|>7@0)QsTePupSLWlF|?`1RqQ_-pjzKM_h6p&DZb{xP6)1 z=E6?nw7Zg@eQP$=7c)A{U>HTjhQN(^aSh?;zA&BNyiB}8PMo2Ia}*IB0&ick%4OV@ zEMDic^NWPt6mEt?ETf2&5cu|mDSs<4;ms>f?UMhzaFVdQ&MBb@04f~djsO4v literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/175.png b/simulator/worlds/sim_markers/175.png new file mode 100644 index 0000000000000000000000000000000000000000..e6e478bc2522ddefa337250222d9d686f970b613 GIT binary patch literal 3259 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwylc$SgNX4AD*BG;o81OJ0 z-1XyseA)d1fhTcg&zL6(M{GIAAMmxhn(=`gV}rpcA}$0r*czvu?PM|1EwegM!Z}B02 zU>HRtgusn`dk-wWc4CUL(bL%e%E>e3jgPQ0Br=U6qC?>KH>UWEyKxUZ*H`h#7mp@D p67$~;tL^3t>k3E1jg)Y^A>X7XDqvY25CQ6yc)I$ztaD0e0sxk8l5YS2 literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/176.png b/simulator/worlds/sim_markers/176.png new file mode 100644 index 0000000000000000000000000000000000000000..4c4825a1c4725c0e9508f25670734a483d425a28 GIT binary patch literal 3263 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyo2QFoNX4AD*DhwV81gtA zwAk@~+FUk0`NpG4ztf~9)KuGSIVT?Q)pjp~fjfhO&nO};1aj_6ZhqI(c-9+_$LIXNX^m!LQp+%4CjR4`UDc?5xAF`qAmg9$i8Hitjv}H%;O$FR zjfl(KAGk-ujnrhb)_5bP0l+XkK*SGf= literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/177.png b/simulator/worlds/sim_markers/177.png new file mode 100644 index 0000000000000000000000000000000000000000..4aaf781ea83d87083fd53a4e1d9f0a4c99d642fa GIT binary patch literal 3273 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcyI1Wy;okcv5PuU*V!HRNG9 zXtCq}w7FcL1TCKIxjmCj<^I7%rTZFIy}!-P@STI9g>w`U7Xoi*8fK^Q>^UYm^AS^n z0plnlAp|zmRyVrOz0F-Sue7>R{p_Fr;k8EM#0C3k`hgFhxKRzBq?tJ6JX5(W8<`5pP7|s&P3n)Izv*A zfkSu{5gh`zw~Iem2g$-!XIAfEQ1BT=q=Z1u{kR9rYiAm!r=6L7re@Q^|MrJROF@z< z0bmi>AGKY);?iol!}d={n=vG|;x?4#Rr{Qt3Te0O?Y%*@z(l=Rrg=AHdrz-B#t5?L*V>O!`Ejdb{w-aI?6g4Y$S%j z`7caoZn-a8qa6J1*9_wr1_8BEM05yz^U?pX%69L8+1I`>>05nC&a2wwJQ{?grXX-A zn(jUDJ?3l5?-Ns2&pR`kjfpJ!Aj*JwSbocL@gMU_ug;jggF(S(6cHT)Z)V0l2(7MS zbesG0|LYS|Mzb%;6#yvvI=((Lqc-h~FW=vpqY03-{0B~e*;S3^v0n}U1nV(y2#+FC qLLj=jiaDW&o8b`4C?Yxp{xMkn7ZDOOx&-Q)FnGH9xvXvmy8Ew7#j>m5pf}~q4xRA(&v&tq@JJB)??rh z9z~>t!0pS-GfJ1S+hqFkSICPqv~Z3hqC?>AOU5%%;c=GXaS#8;FbJrPB2q$N+j90j z7iP+RuzP*ZxcuKtZ~JF_qv?;N><3JL`dgN<9|#^zd?e*PV9K+3<-=bwzx1hS{h8%O zj1DsxMiB`iaATg_2dC_+1C!594d3@_a;@lS#w9i5_D5Z2zIR+byLdG5k(l>xSY76B hFdS{^k<`%r&AjiskieN7rg~6!#M9N!Wt~$(696RhH9Y_T literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/18.png b/simulator/worlds/sim_markers/18.png new file mode 100644 index 0000000000000000000000000000000000000000..1770cbad6f18c80ab212937a7d985ccc7dada4bf GIT binary patch literal 3269 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwykEe@cNX4AD*DhurFyL`G zxGUxV)VrL&1UZaO?v8foV&-4_mpSC`yf_96cLoKYQAAt_yqP7rIqlei^fwbV^%yvW zM-eF@u-i}ngVXOXO#V^I!EN($o+aBIWo1ZY8bw5h!1b4`a$EiNKiJJ%b9OZ7NKHB5 z&~sY`%)8F=#>p>E?idY2(sCa-0Nt0Z0R~{&yqu2z^|QGd4zY|PqC;TaWp10Sxp5CX zt5Z%bpVxCchCx7W6p<1FWx&vD{dQ)Csqr)Kw0~!o*Y%7hL6UMHFbVFtV6~Uw|A+Lv vVn&A<45NsI5V&z`xj92$;b^##l5B3sb9jpL+Vr*5fcht%u6{1-oD!MbDACwnjTx&$i7>oS}tt6p<1F zZ(p*?ZC&SM8NMv0>P+~~&&Kgz4fPl}ghvt4A#nRLx6P}VnVZ)6JS?tzmOL7IBqbhT z=o!2Qhu+V1$L~BGtvyJrJYluRkK?oA9@NG#2&j!BqC;TYa`B2Qv*H-;9c5)mWEw@J ogurW`e1@Jv#?eMH2_bMpewmU8zYqK7Z=f!Tr>mdKI;Vst0CU1^uK)l5 literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/181.png b/simulator/worlds/sim_markers/181.png new file mode 100644 index 0000000000000000000000000000000000000000..b0929ecad04c4aae83d2b15e80eabdabd3798bde GIT binary patch literal 3252 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwywWo_?NX4AD*DiKS81gtA z{QU4=beYx(_k@j`ey2%I=y|xXw60;*{giY81D+%)RIQthk2#1&j_e7)BAXA#h_}+=Ia9r>2yqo?0$f{9Jc>%&-5}F$@A~ zqllCc*tT4};>s?gr!g}>tv&u>e$i+mBqa}`C&EA0;tVaEqloAbc>9ty?uC#3hb8{D zX}KEwM*ObrH%qi8e)Xcz(;N|%{49NWR5;4_Md4uLoP Ys_9}}`LoWh0Ch(^UHx3vIVCg!0BK%k@Bjb+ literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/182.png b/simulator/worlds/sim_markers/182.png new file mode 100644 index 0000000000000000000000000000000000000000..a1374ad2a5bd22791ad4dacdb7556b40262c404d GIT binary patch literal 3264 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyyQhm|NX4AD*DhwV8uBn4 zwAk@~+FY(rf)-Ei+@8s{w`U7Xoi*8fK^Q)btn|9c5)m zWEw@Jguw4_On;vB_(UoPw^eMiFJ^R@!7z%54S^f`_8wS#?!**RqtvW39W&(*-`~lg z;4_Ly34xsZaSvM0otXg)!xzbEXKZe|7mg-EQt}`u5pu7+&3zy^hCx7W6cHT)+w%24 zI6eQy6rI{M5f;=M54ORQtK-5U6Y7>FVdQ I&MBb@0HBe91^@s6 literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/183.png b/simulator/worlds/sim_markers/183.png new file mode 100644 index 0000000000000000000000000000000000000000..406ed24b7761f8de9e7c27e46beda68361b2e8c0 GIT binary patch literal 3259 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwylc$SgNX4AD*A8Z~IPx$! z_TT&yEa(29{o#a~>!F+;n>X~H`^&Uy-*$0^ZyXFQoTG@i5U@2)K6ggqheUZw7dOKp zmQh4X2+VuS8uuzs|G=uvdk_3BWOSIpFp7u`fg8XuleRKWes^NZ!s8$OcQPpWj3QD( z;LXjr2TRYLm||^|`oeI_X!wzoe1L)1aR1Ik&GeL0XQJEA)NE4JW8e@TMMQ@{^)~T} zmAm&I(4PC2^lh~Q5#{-Eu)hk_EAe#ob6Mw<&;$UzWG-d^ literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/184.png b/simulator/worlds/sim_markers/184.png new file mode 100644 index 0000000000000000000000000000000000000000..b2847f48355bc9df67e37f84fe252bbf39ab25de GIT binary patch literal 3260 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyv!{z=NX4AD*BX0U40sq0 zp4a}neNHx$W%(N)W;2Dbxaa&Ue(m4OP|(ldFk=)E7XmltNp4O%b|8DsL`^*g4&hNm zN(kKEF8;&u`8TGRt$F$%RKyutI7boDA@KG#x6P{_pGf84wu;o;(Zok;-UBASiYrnx zi`bFu3zII|txh+rpDb~@TBP9fYp=bZ% zOi!e7^1YO2r}w-%yn{i(XB3eV0y+2N9cZgQ6eGA{Gb0!DaPeFVdQ&MBb@0Fn%*Y5)KL literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/186.png b/simulator/worlds/sim_markers/186.png new file mode 100644 index 0000000000000000000000000000000000000000..cab4721044b9a603fdcc462e2b84917f19f215b1 GIT binary patch literal 3265 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyho_5UNX4AD*ADhRG2mf1 zc-isi@-@8)a@-P+s&;8H2AN%WVD-OY)&I-f48<%Ahei>RA@FUk;p=k}Kct?Y5)o%; z;T%PzguvUEtZ^@V^gr0WJ~zX<{@JDEcLj_NGZ;n@u_16{UfhG$YhRfBtuAxhyj=b7 zjI165hwvyOB?N9?=KgSUX50hE>Gw9h>-niR8iFJxAYky>yaMLoC8epS=K7aC*S-IB zhII^sfZ8Y`Is~>Y7k@ChIQ7&nqt^YWSQ!$TMiD6?P+i4n7kQa`Prv`S?9tpya_-IY z)n|DBh^fJVaTJjd0vk$~nKK;Q!JyzXiii$@H|z^@MFcEPawLNKCZ4W-F6*2Ung9-s B561ui literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/187.png b/simulator/worlds/sim_markers/187.png new file mode 100644 index 0000000000000000000000000000000000000000..640f08d4188b3ed96ccfe03899aa6fa276ca4cea GIT binary patch literal 3273 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcyI1Wy;okcv5PuQhf)FyLW0 z_145u5v)f5or;dl@#kGbs3sBH}{e%}mMSv||U-?@ZLxW8e@T zMWlp4^)~SZ56<;i8Yk~N!pe}yG>V80f%D&(*6&gdK6he@;{0DJ^Q+GAh%>Zsjv`V* z!1gxxo>w{gAC_4gJq@1!Cb)m*L#74;#!*CU2y95NZVbQsmi1nV+p;q?n`UnRP(4}* zl2iu(OTZt^=g!QqHcEYFXq+!;^yPp04&rK_(JC4q0&i}{J>cI3%*~n44AWCiZT~k~ z8j?~D0;>&ZQFuV>|NrYT3<7GSh?Ed0d-sj)fYfLOOj7&fFGEtUxWFP-)rFuwil?ie J%Q~loCID~N0N4Nk literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/188.png b/simulator/worlds/sim_markers/188.png new file mode 100644 index 0000000000000000000000000000000000000000..c8aab3f6fd56e8ee057b0f6bbbe6755df9c2c0b4 GIT binary patch literal 3264 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyyQhm|NX4AD*DiKGFyL`G z`1#?#=x<>g3Y_LLkRq|AW)>b2CcQ&xlUHSM>6HO(!?QA(l}@bO?O=!qjgSzHE(lu%7L+KK;E< zqhlBZ)J74}A+T+^c*T`jaSv+kzMeeyh3WlcrUnDXQA9!rY_Q$Suo)P1{!y2?_w;(c#c=h-2>{Hc)I$ztaD0e F0syK>hz`T7iR1Q|GlM-g!$P@Q&c&P>A(lI1B~+zf|U zMiD6?Fz+qvjZ*hzb0%uuuR6mc&d|a+iii#Y+uPiK9Lv+r%#<`vdZGUKdJKbr+9)C= z1it0ye^|A9?}6EAa>Z`Tcxq?NJYF-Ji%BcQz*%^^c*W&2$>&Z?QPd-@)Euo|AR$n_ zO}ygj%{YdCVxxs0sbxQ~{M+-Y&&N_Z*zW6$ncID8XX+O+I?P}gMZ|`{jh)8HZp#jo ypZQ}POG>#pTFn4MpzPf@wgXb5*_Nbi`^i>y literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/19.png b/simulator/worlds/sim_markers/19.png new file mode 100644 index 0000000000000000000000000000000000000000..21a9b25040ce5ee0ae38e46583349bdc85f41e86 GIT binary patch literal 3265 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyho_5UNX4AD*DiKGFyL`G z_<7>L=x=5l?70sr)fw}89DK5H>wm`3|I5W0o^dd=aE>D4Lclit*c#*H2U5RJh=>!X zV>D>tAz*u%yXL~YxQ4*&sz&pj3<^G@i0BY_Gc)c%=fgSp0nYpf6mlkz&MIX z2!RdhRgK|mU$X8?m!DbmBJo_!na~&p0ku&?bO?O&(f{CB4a~d1P`p=i_4u5Jqlu7| zJP1yKZ(p+hxEsp9d6{^{WNwB-ETf2&5SaIp)h=VMQ1W8Ij*X)d+ zoe5P{(mB3oOAt>9)S8Lp00i_>zopr E0QgI~m;e9( literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/190.png b/simulator/worlds/sim_markers/190.png new file mode 100644 index 0000000000000000000000000000000000000000..db9b2e6a35c9f1aed4367f0de56baa5c0f9cda25 GIT binary patch literal 3262 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwytEY=&NX4AD*DhwV8uBn4 zwAk@~+FY(rf);D;+@8s{#QN}}(tQo9-oIpRuw-IL97RNi!0&T2O4E62dWuuKxET(y zj3QD(;M*6beyi|hYqWzOFo`p?aE>CPL*VU8)*okQ#yzOL_J!&FW2OcJ#!*B<2yC$3 zdw_fHOV)eocO{FTr|Q+7k=0}15FSNDhrsR2+%{Rh`XB6`H!rLBzrUE#VFtq}A~pnW z%!_*v`25tA($rJ#A5Z_&9xeSyE&PE6VB8BI{SQk__a2!1ZqFI_zeb~hNKzmILr`w( zGVz2d-~Str1|3O32UNV)crT;DG1k$hGASWoubUwv@N)i}d{D>4)78&qol`;+0J~F( A1^@s6 literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/191.png b/simulator/worlds/sim_markers/191.png new file mode 100644 index 0000000000000000000000000000000000000000..5749c45111aeb46e2ac28e82e328995ac5a078c2 GIT binary patch literal 3259 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwylc$SgNX4AD*BUcf40#w1 zTI~2gZLSoP+q3#~sRb7jnL`{POtPF`vqloAb`2CeBeC>7aJ0)@Y1s5;0&#c_E{@3Xk;?nGB z=7xvBws`#yjpxqHFg1QQyJAy)F{8r_hEYUp2;8XKdmuDD<&@M+Cg#z!2TxSQFn|fK z;A++01HS1gUFyHLudgy1tv*PtJrUK1bFxk3Y;J}_ETf3%5cu|*bzRhY@gFwx)|?%! pd`WG4yxwz(tQo9-rweC_|C!5!a0hF3xT&Y4YSjD_8gO(`G~2( zfN>O&5CR)&s~PKx-IlGHsA-%pS#@Uh4h99EQABhIwjpx_LlX5=4jF*HRnY?|HgK}bTr&Z3b)^keZ69QV);@wp#F)ctDnm{ Hr-UW|a0~F~ literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/193.png b/simulator/worlds/sim_markers/193.png new file mode 100644 index 0000000000000000000000000000000000000000..80ef0ac8f3dc822e03439f941374e0102d4b44fb GIT binary patch literal 3265 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyho_5UNX4AD*DiJ*G2n4H z`1#?#=x?DLe@~N|Fz0~t>pu*k`?rfTyc1;L5FSOug}`m&$$hA`_9jje0<*K z*=PzRB?Cf|-*)kei#zu+$nRiK@EJvw`U7Xoi*8fK^Q>^UYm^AS^n z0plnlAp|zmRyX!rDF(2C&uc^D zG`pLtcQ7dUj3QD(Am@JEgT`xTW*8ek^96?Bh5z#(j^<@jiZY~(tZ3Xn^QYmTx!eqg zSVj@iA@J=jtK5q_aStl5y=DE;IvRMSrX67T*=6 OPsP*K&t;ucLK6TlRoj06 literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/195.png b/simulator/worlds/sim_markers/195.png new file mode 100644 index 0000000000000000000000000000000000000000..7c4bc55695da6b00e81405999f0428a0540cfa06 GIT binary patch literal 3270 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyucwP+NX4AD*DmHBHsEnM zxaz|{?bX~S@{BxM=XOUoY~isfcq_l))%Pz<2P~Nw5=Rk{A#nZ73{zvaeaGZxK4NMx zU>rpxgusT^Uzqr}`0F3w1~Se+9t}TIk`E~Gw%=MVzT>jg%uk{IW}gj>?bABA84j_G zB2q$N-DPf@3$r9QryuM2kTU-i>uA|WN(fwk$(py-Pyd6BvHv#rWgqOs8Cp0;5z!$K zyIlOiR8ynWexI2?#qK>HEdfa>0fDtGtPrf2XKM6xb={dWlX(jn9cD0$B4R_}Mrl=J z^xDhZAMEX)1@B}~@EJv>onx)1;W literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/196.png b/simulator/worlds/sim_markers/196.png new file mode 100644 index 0000000000000000000000000000000000000000..c767a64f9f127e385fe2e3eb5c8a3b5d89b3af6b GIT binary patch literal 3255 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyou`XqNX4AD*DmHBHV|Mv zXprz<*I4w2{{z>1GueJ9NOwm+XJ7Gat{lS|K?V-tQAAt_+%BINSw63Eec`%2kC++^ z7)KEaA+W*LwjnaT>cH5-W8fJyH`;PX@3 zuTN=jpQm`1l_8O76cHT))l~-;U;EOaKKCUf|E@2G@BB=O|Ega+nt@3vzknI|gA*_V zPrf!aeBZ0dwW8t-Eu5o>ln{9PlCfsfGIoInqs1R7#UH5bYXxTC?;lV8(;iKAq-MIc k#(NngMr&M>+PWL;*=Gw02F0#;1nQA^y85}Sb4q9e04dqX6aWAK literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/197.png b/simulator/worlds/sim_markers/197.png new file mode 100644 index 0000000000000000000000000000000000000000..fe8260210066a703458fcb13cc787421ffe41d23 GIT binary patch literal 3263 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyo2QFoNX4AD*DiKGFyL`G z`1R4h=x?C~@{J0cD~)+Q{2nYUt!r3y|28+ncMgUY&QU~M2)vzXn4QK`b4+sPBc=ue z#!*B<2yCdWW_)L*9DMD>lziK?Hg1MPETf3%5cu|%HLlcc*_w%(&iy~9EVTcus>i?~ zJc>vOf!o`~E3WR^dtmmyFEfmdpLx&E=@?CgBxOQiD%|n9$0t%bxZ)fuLn6~CB02%!3O=KVln}_dANL?IJLQz*OrtL|W@`RfAHyJ^Hj0Q2fo=KvADmu)WAe8GX6H-G z|DE9xXK3LZMMQ_d+uPh9CL0^2_WI1MJiXlRrod?Uk(zvfLAd7vuqK(DCb!vbSR%-4{)yqCPe1?XV*syKvK*9ZRg(dHe`>68%g2zjsK6iIRBv@BPCEj N#naW#Wt~$(698F2OQ-+< literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/199.png b/simulator/worlds/sim_markers/199.png new file mode 100644 index 0000000000000000000000000000000000000000..7f836f74829abc45e7cc607964847e30427cdc8c GIT binary patch literal 3263 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyo2QFoNX4AD*AC_$F%V#J zyd3aP^RnO%_k#CVLOBZ*7<*%Xv#$*Lg*6%5LIn#Rnn@QgF&wOJT1k^?m(IN27hyRCDHZTePKBYa~?&j)$XJkhcB0Pl= z!vLqk3Z=L*UI!xdZjr xzA!M2COuM<-nMUFm=b!p84j_GBBDd!AHyUa5yAIKo6mxJCZ4W-F6*2UngEFH$w>eJ literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/2.png b/simulator/worlds/sim_markers/2.png new file mode 100644 index 0000000000000000000000000000000000000000..b69d706f5efc0ba62726db9ff932e432848b612e GIT binary patch literal 3269 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwykEe@cNX4AD*DiJ*G2n4H z`1#?#=x?DL^0hM(6}`qrM_CyX znMM&QA@KVfQ@_=A@rp}QGe50TpKbU#xoro7g3l--Is|g=$2|zlPB|qx)96X4zu9NQ z^^ceu3>Zfd2_dkdwz|=M?nF&sFs`oH^#9aoJ|?Lc17+eUP$rg~`RL@nZ>!Xc869RY zj3Qz~;Ksha4D;7a)J#t~b!NLyZ>6v}Lks69A|(Xg-sb*ra_8Oy&29Tq&-RRlASq!7 zOlsiZ+qIX0QIEJb&uHTi9s<$Nzp))K9St{lfDps@&3rvsROrbZ+3%qKiKnZd%Q~lo FCIIe54#ofg literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/20.png b/simulator/worlds/sim_markers/20.png new file mode 100644 index 0000000000000000000000000000000000000000..43afd2da8c30b42306364647cd4c99e541b732f4 GIT binary patch literal 3258 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyqo<2wNX4AD*BWyV8}Kk3 zTy^1}wzjuK$fNzy4O4h+cAS$B_*!4h_&|=a!C({-7Xlk>jnmHdG+wt*4vt|EP#Z<0 zguu3Z{SPaQjZ%Ak8X5H%ID|(L(IIeqyLiPFshLG?%l4c)(=YkU*tvh^L#EMuOIiqQ zsI6{vpF2@AJLS}Fxx>?o7#(IXj3Qz~;Ksha2Nqv@%lcsZ4h99EQABhI}T zVRqUXj`?q*cM`2}G>{=701Cjs^psOlGmXB?2(P^QfBtCMM^cjwT=1>E&295CwEv;^ rXaXcPW97`fEzNL!wAdpl+}bP0l+XkKcyLnr literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/21.png b/simulator/worlds/sim_markers/21.png new file mode 100644 index 0000000000000000000000000000000000000000..9821a953188e8295a703b3a3d66dabb8a03d7936 GIT binary patch literal 3266 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyr>Bc!NX4AD*BVzoHQ-@5 z`1#?#=x^&EI!^E`TgIr#rxW#DKH%&BZ%hYlnHUmB5s@KKopxrHB=h`2w`DsR6nsVz zDIt(^Kkh+bddexOnTOv0`9FUrDSAhP8XS7rDW@c78l^lt*;ex^Ic+ovlA0_)f!KQO z%#6Ejv?l=FE1V-k(CFfk;vs0*0VX*4?-VwKkEnxfu?zj3QD( m;M&f;j0L@;;YL!j`NyzZN>r%pRrVQB=fu<1&t;ucLK6V)-(VB~ literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/22.png b/simulator/worlds/sim_markers/22.png new file mode 100644 index 0000000000000000000000000000000000000000..f7a1ee61c8e0d5f27b15afe0c1c8bae3690acbcf GIT binary patch literal 3274 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcyIL{AsTkcv5PuN~|?Y{0{C z@Ur9I?Q23N%y(40H3>)fZku*& z*WLr~N6SHyssT_TxcJ&z)(6{nFevzpB2q#iXYOrjhU=r@MpCkQ!+d~IoVQ{g+X+x7 N#naW#Wt~$(69BcOU1b0O literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/23.png b/simulator/worlds/sim_markers/23.png new file mode 100644 index 0000000000000000000000000000000000000000..0219121ab3b9c443313393919134f6fcd1892945 GIT binary patch literal 3268 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyx2KC^NX4AD*AC_$G2mf1 zs1fjAS6lRn-~oZ+do$TQe)4gz`^*|r>#xsnMv#F+coY#A0=LVLt+7siV0vz5I+r13?ErB^X!&m=+ehh~Pq?yR%J|1`xJS~y1$ z(IN2mC9Bj~0BSmirq@mzgsh+rgmVGm3}~fj8{C*N6&eelpSpbx%B9 L{an^LB{Ts51Nkk+ literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/24.png b/simulator/worlds/sim_markers/24.png new file mode 100644 index 0000000000000000000000000000000000000000..ecc7bc8910c8225be844d5fbac75801de02363ba GIT binary patch literal 3253 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyji-xaNX4AD*BUbq81OJ0 z+?Dcw>Rm~ugIS--xjl}3$apKi;8l4QV?rN;!;DcxTnOAKO*>;{%x0G?Kl2e&g8}0x zA|V7eWLGu1zf}&tHc|6_70>@E+zf|UMiJ2=udVM}O z-IvK!omswvLBVGf5gh_=X2m^d{q}{)-|F*>(iE=mJBijg8p^;B00-Z-FHG+@pYQo- zynQtENKF->01SM6VoGhwspWHiO1vv(9Bs0Y5CS($_a5M0`;zs6rXB-_@F*fB1fr{} jm=k(N>sylQ+kXsC-U$mlkzOzd)F1J5^>bP0l+XkK|L4cp literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/25.png b/simulator/worlds/sim_markers/25.png new file mode 100644 index 0000000000000000000000000000000000000000..2bbe12c572f5942a8f1731d57dbbc4aec3411c64 GIT binary patch literal 3272 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyzo(01NX4AD*Dhu<8;ZCd z)L8L<+ElJT8x!W8m^YJc-IinQYdD^32HC zp67fe<7n(OFMVwOhrrnhQNl}-wg4kZeiC>oVsx4jQZ@zhnD&b9KxfBln}VRo&SJ(dP-{VnlpD=pO0o? z(z5W}nKLJfR`k!TUnb7b!a0ga34yi6zgZ5L#xV$}jUu8$;2Zmb4rz{i$L1^nbx}NB L{an^LB{Ts5zN~l#+wzs=3?or9r;a}*I50&iy;W~cG&IVL&t5mSQ! z<0v8_1UA%GH@eTAsF|H|>OyiFPpzbUF{8r_hEYUp2;A7W_rT(7C#D!1Jq_)zygGev z7dOKpmQh4X2z-0XDp#7P|6$3@<7*~rI@jDh9>XA@Hj0Q2fo=Kv2V&DxPD#x?^n5hv zNJ=@tpmR7lQ*v|Ku|1umIhWLe3z&0%oZq#Vfl-fvLwFPs9Rjzvi&tC$hFgueIB{*9 s(Z(P=1lAT;Ge3|T4L5jz5X1P(sGTgz7yDMy0n{JyboFyt=akR{0C%u(W&i*H literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/27.png b/simulator/worlds/sim_markers/27.png new file mode 100644 index 0000000000000000000000000000000000000000..4f78b699db7b8821b81f95de2cfe9a5ecbce590e GIT binary patch literal 3267 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwym#2$kNX4AD*DmHBHsEnM zxaz_`?cLle@~k|DA9qJLY+k0U5>ygHc3W2yCcLJ7Z{ngIcrWrYcg_6QC#JaD_HZ*CVi`q5hrqWlOz|&f#Wk2;`@(ep_-N`QDfOWgeUj~U zlK-DEjaIv)g}?^ey$8g1Efe2y*~;kY1%2z!hW(F51Ch9f3#5ey3cXp96Hiw^mvv4FO#o@*z-j;h literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/28.png b/simulator/worlds/sim_markers/28.png new file mode 100644 index 0000000000000000000000000000000000000000..c0c75c4199bd1f7ccfacd2cdb81d9c35c04eec02 GIT binary patch literal 3271 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwypQnpsNX4AD*DiJ*G2n4H z_<7>L=x<>gv|1Vh^ESVS*M-h=BaDArX>oXD+y|pP_+zf|U zMiD6?u|H8z-WtsSc?%I@7tMA<(4Lu^W zEnFEm0HH}RbpM;s7zP2gQABhIl=>UgWKBI`}5O^~y?m-wZ z_^jW)WS#ds^nHEXXeB~w9SW>OmdKI;Vst04?B* A!2kdN literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/29.png b/simulator/worlds/sim_markers/29.png new file mode 100644 index 0000000000000000000000000000000000000000..1a4710443930c151856c5e13031a79e5e40ee930 GIT binary patch literal 3261 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyi>HfYNX4AD*AC_$G2mf1 zs1fjASDW`qaf74ky_swtbNRW~eP#`*y&uQ0!<|9FXA}_^0y*b4pd*GR!a_Wq~+2?8HwrOqL42M`o5z!&=?JeuSg{9Sv>XkmTM}v;k zlmiSsxvg%?=1kQ56Kec_lcF92hwvyOIs|TS7q7S?IdhZSvJX3FzW=W^8hj+B9$@f2 zSZ8dM+UxVMcr@rp3Ob--aOjyDJ?-(4tU9xL2XXcFXu}2)0y+2N9t6Jr#`Nc0`_I)e t3<7GSh?Ef6cJ3{2L-uI6k(6w{G0!|HEd1%xq$*Iq#M9N!Wt~$(699k(*nI#1 literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/3.png b/simulator/worlds/sim_markers/3.png new file mode 100644 index 0000000000000000000000000000000000000000..0378c4879de810eb3380a56d9061fa1e87ae7a12 GIT binary patch literal 3259 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwylc$SgNX4AD*AC`BFyLW0 zc-isq_BA0B<~ts}HKBF@ml zIf_ULfw#B0ZC>4pdr*7rE$cj!&wuT|rsy$n2#+G7L*Vvy@ro=Qel2RBnbt zETf2&5cu|%Rj$-+*_w%(1_xLf5}8I3(IN2r8`Ga>JwB1j!EI-3ZZ6+JTzNTKu|Pr~ z=YHIS)^lfOm>Q?GsQ+Fa!yuqGibx58ZNR{rsOg+GFXzPaeP@=R`I9zUb&ynZf~t;% wrPYiTU8B_oskP>{oqHJzdPl>Jq;RWaurL+ll$vFVdQ&MBb@0D<2zG5`Po literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/30.png b/simulator/worlds/sim_markers/30.png new file mode 100644 index 0000000000000000000000000000000000000000..15272dff3e8ac44c9784e7541f48a6e4707590a4 GIT binary patch literal 3268 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyx2KC^NX4AD*AC_$G2mf1 zs8R4=SDRPG-tkOjx|D)0Gxxf`tRc1j`V40T890PT5pf|<{jMjn{Mdo)H`>853<7GS zh?Efc=A&P5WnSEa&udRP`ymtBFk65;1Vzt7kJLWiM-d4jupzywaq+h=OzLa3gKZ*bkCuNV z75|{ZFX}RPjhz3rXQN4v#GJQb>vD63V>?ERJ5oa64SPzL7{3C?($%2uiKnZd%Q~lo FCII>?8WaEk literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/31.png b/simulator/worlds/sim_markers/31.png new file mode 100644 index 0000000000000000000000000000000000000000..39d6615d5b4d5b4e9390b6747814ec265f6536e9 GIT binary patch literal 3275 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcyIBu^K|kcv5PuN};N;=sdj zaCY-2|H!@r_KjPvgmO+;9BWeXuHND5|F^6SwoD9(qlm~5xPE4au`!$7W4W1+m>LWi zM-d4jupzsev97q^$5J^s?VeG}v&p=Lj1DsxMiH?gaAWJ<1Kf9SbK7J-GfYo8wY_ep zq#gr@@F*fB1a@x|ueh>z?}6WQ-m*qK9nHj~lwrUe+$e5kocwO4p>o?jqc5jxI=LAR zv5X?3Ltx!oR=HAO3Y=eE-Kc)n=2`H6BXNcn&QU~42*lpz-g5z%g(sgoF=e5@+2^>Rm~$gGxVlvo5jtkTF+&!K?Br#)LiwhZ&=YxDdFp&p7R_B=h{jbv}iRqrpa6 z2;7(#_n`IMT*KGrBre4;2&j!BqC;TYa`B2QGvgk3#{Zg8nto=k`tC1=ZzSZPcI#>=~Dk~F3!-xIf{r5fwwPN<1+3_7QgG+`RLN*kNJ8G9KxfB z=n%MlnS0L#P&Ur4YE-}b(|CTB(P#oBHU9zApj_!P@q{b0;vUqF)*~cUqu_dERvg2< uqpS>xOrwaD5P0pA&(Jg4>LaPIa6?{MRz%=cNkSl~W8&%R=d#Wzp$PzI7FzKD literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/33.png b/simulator/worlds/sim_markers/33.png new file mode 100644 index 0000000000000000000000000000000000000000..ad84316bb70e8a3bf2185ddf0d5e599623e08514 GIT binary patch literal 3263 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyo2QFoNX4AD*ADhRG2mf1 zc-isi@-@8)^4t<4Z`OG=T&cM5!0La)s{h-?8Quvpa0rhg;zHoIaq_h@5Y&gojK$GZDsvTZiYiFqllCc`1Y1H?!}p&NaJLWVn&A<45NtH5V)~#?*Z<$x4A!X u$3HtiT4j)0V{SY5mbW2$G~7rEw{OhF*&>3y;q{rIo{6WcpUXO@geCy`r2zH- literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/34.png b/simulator/worlds/sim_markers/34.png new file mode 100644 index 0000000000000000000000000000000000000000..c7189c82287263d9aa06a390a12322b3fc69a5fc GIT binary patch literal 3268 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyx2KC^NX4AD*AC_$G2mf1 zs8R4=S6lRn;DH52w`Q_=%rD?x_m?%Kc77a#g*$_S&nO};1m4V)+?;mofa$jrBH|1! zoTG@85U^b?{=?z-7pDBE<>D2W&zz3=I(a8?!9JQkAt8|ScBY|n@H?Z=M#g*o-#@!& zKR3f6mQh4X2+X_8U9-T}IPGlD&Xb@dXrRZyAv}tR4uNVv{ST{Zs~XK?zZ(7t9t}K_ z(hex_8n2z1VQZB7B025MpOo-IMu!;;qlknMxKUiiShv}2*_(-)#`h21AFV-1svIFT x$OAd!|30G)7*gABYip~RA4rUb8%g2zm*LzaNtSvy!)c)IiKnZd%Q~loCIEhjMn(Vt literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/35.png b/simulator/worlds/sim_markers/35.png new file mode 100644 index 0000000000000000000000000000000000000000..02bcf5682d55f8dc8d57d2584ea15ff62a502db9 GIT binary patch literal 3260 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyv!{z=NX4AD*BW~t8}Kk3 zyngua_BGju6a?SkKhM({Jsxv&|#Fd(( z)e9s9Y;SY#d6lEjuz$4jAi3@YRvznjDF>fBF~#3D?U`&r4`Nl8^sEs00 zLSWl+@gGjhq$iMURf0~Okv~Z3hqC?>A zOIEq9>wGN3m+g6Nm|yh#c+8{G^haX$LnOf|-u(;Dj#eKe)tX>-C`njxgN@xNA35YLs literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/37.png b/simulator/worlds/sim_markers/37.png new file mode 100644 index 0000000000000000000000000000000000000000..168d819a1e02871951573cce63ca854183abe8ae GIT binary patch literal 3259 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwylc$SgNX4AD*BG;o81OJ0 z-1XyseA)d1fhTcg&zL6(M{GIAAMmxhn(=`gV}rpcA}$0r*czvu?PAZEl;aULQ;4;5NC>T6Q;&j~0C-mHeP`Z-uc@YOjxE z)tS{ie`lH(GCIs)7)2z6z>R%-4~SbCCto`;W#;iSds@%$WKi%KMMQ@{&i%Ltfv>+Y x-9N6LT|AolNX&dUthSpotQ*a>Bo$gWw`U7Xoi*8fK^Q>^UYm^AS^n z0plnlAp|zmRyVrOov4|ea%%ONKQk29|N5`Tz#%+}hz^0<+r=xcNX;yATgGFXc5L>& zPkN&PNNV~4hT((B#zv{VJ~Jy%PnhX%c7&B7k!chW9Rk0^*oqTZXOA{+AR+MfHuoRr w*WZ{vh>QjtscB+uaW(S;snKvFDct@tNGA#lG8U~l2kMx3y85}Sb4q9e0DrMxZ2$lO literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/39.png b/simulator/worlds/sim_markers/39.png new file mode 100644 index 0000000000000000000000000000000000000000..069b1ba68d26562469d9b5529617ffd54a9a67c1 GIT binary patch literal 3279 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcyIG*1`Dkcv5PuN}-)G2~%5 zcscQte`MbQ`^KiLp_~(D9W~qW&3?hF|8H3vs#zEgjUpmLVBbu`>@*&mp9G%fcA}>7 z{G4aH`L=0oqtywK%@~NXYx5+Hjacs)em-9^+L$4+EqCMAc5{Yxqm4k4+Q2vXCq0wm Ua^lyX2O&5CR)&s~g?t-sY~EXKIxCA~DVGS!fJ{fZ8Y`Is~@m>mP_sPdOzu^U(dB3<^G@ zh?Ee>xgYm{dF@QY^t3Y_=C!A<{61g6=rDs}6cHN&H}>s4u=v_rR=e|O^sSYH_xvAC zgrwy`PznqL=U*e!v?H_kJ&heL07)(Xfn}h>!I_eq(~j-wbP0l+XkKe~FWs literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/40.png b/simulator/worlds/sim_markers/40.png new file mode 100644 index 0000000000000000000000000000000000000000..38582e43209ac2047eaec70607cafcc199777e7e GIT binary patch literal 3267 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwym#2$kNX4AD*DiJ*G2n4H z`1#?#=x?C~^4yMTpVOo!2px8Q{f8lR|8{YPcY+KY!lQ_|5V&oeeC>=xMX#~ZQC5aT zrcp#n2>kxW)NiF6eC@=P@Y*wHw)03<{g>1uF5E{GC?o`KZx^q)A~mzfZ5dDH)#D!w z#Ti;SM-eF@@b)%$z=pJAb7mUmKVoVyU>rrnhQNl}>PG!7dHNrgd|ssazG~)Z-X*Qx z1n1qIdk;*$_LgeEknAN~;^y=T6kDi=55PaEN6T5gh{G z-m?BUziaP-?;lUwJsU0kNG$v}l;-6##5^7iH&ViFgMH|IaXx*4e|DhWiKnZd%Q~lo FCIFg&9`yhK literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/41.png b/simulator/worlds/sim_markers/41.png new file mode 100644 index 0000000000000000000000000000000000000000..6c5b405b8e0e244b93212bcaeb81606554ac77c7 GIT binary patch literal 3269 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwykEe@cNX4AD*DiKGFyL`G z`1#?#=x<>gG)MMZf9z{fl!0qkg6<2ocJ@C8c)yZ>jS??WZ zWk_TiMWlql?{7@~R?5NGPE1jpU$x2q@66FONLm(zr@@L%X5tJjoTG^75O{l=d(VZL zlAF_x^+;50l8palsK=l&TKI?=!2J0)CjKbnZU>S3j3^P6w`U7Xoi*8fK^Q>^UYm^AS^n z0plnlAp|zmRyVrOz0GZt+3Pd&XNqzD!}l=^0&1g(=n&YJum6F6?M%b;lv5Xy(|)AX zKO0SiB<4YIB3uhhgfD&jEsc}!eL2~-gF(S(6cHT)Irrlp1Oii_)XY!oj(=DLw* z7*p_HH(u++ac1R-Z+w_9-Q4ivj{bsI)zyp%{R|E>MiFr#aKqU6*_ock`p11fg^Z)Y zMp_8m*u3|^>UZCm@~xDEYc`pQGqiAyBBDdU_BQvPS3N#Em4lZZoA8XO!GLiTkq`nK z(yJToU!3UyhF_YT@nP`yn&;rh+E2fqJb85+kh t2&j!BQbM5Y-8Z%aQlpI(lGL=x?C~^4tfL{-#My5IXFf{huLp|7C85Vitx&qlm~5m}i`PZKh#CZ*58!_h_(@ z7y|QNvc_#)F8;$VyQHTj zhQJNuy$t^&FLT$-2L@p%|6`^G1IAHALI`X~uVPGls~mi7qUQ2rcaE?!Br=U6qC?>P z7pC|OUtsaK+OBQ1>?678&w2Zjx4}@4fkSu{5gh`*nZ5su3jEViI}Ylfc)I$ztaD0e F0svECs&N1S literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/45.png b/simulator/worlds/sim_markers/45.png new file mode 100644 index 0000000000000000000000000000000000000000..f4d2a10b507d704c9741a53bba079273116bf141 GIT binary patch literal 3271 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwypQnpsNX4AD*DhwV8uBn4 zwAk@~+FZ6z!WL`p+@8s{JW$rx}b{RjrBUyaZ-S+0v{+)+eNAoR- zAy8e_=)U$P>%HgaCePIl{$VZ7(84*2hz^0bFInYEmx))*@2}cqy7$2E9SjOSqloAb z$nn?zu%a~e)J~(PlYk*;o&3(`rs`-hNNOzrED3F1`RIRG0!o4vo9v4j9cD0$B4R_} z#=N)(f!S4zb;q^7>;^eX0rp3$}+Nlo86hHzm~!PbuL`$0VvPgg&ebxsLQ E0JelG!TBc!NX4AD*AC_?JMu6b zoPGG&{7)84|5+~GTIba;XX=FqJO4MV`hT07p_+x^&?q7@1m?{&Oi$z4b4+&TBc=ue z#!*B<2y95NW-Pn=meuZMj{b+*7zP2gQABhIe9O^4ux|6-1L}7tYOep4qQ}4?Jc>vO zf$DAIKUP>9J?-(SyqcVL&*4}|Gj!N>_`ebpyIN3-`EaFi8Hitjv}H%;4gz(qB!sV9nbcGIwzj4elF{r G5}E+^jA@ww literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/47.png b/simulator/worlds/sim_markers/47.png new file mode 100644 index 0000000000000000000000000000000000000000..1fde55b0595a33c531ca84d37dc75411fccdd998 GIT binary patch literal 3266 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyr>Bc!NX4AD*DhurH4tDp zsIlY!w5e>9_?s_2txT8Nv6=Ps+P|zJwf_1HX9O8Ighvr^A+Y;gPh|SB1KDqsgJT#3 z)J730AyDS4e<1$bM9tSHra1rEbbB-aNlHJ!(EG5WHszF+(bLd#Hay>VGAQ_rB2q%& z&8)Zw%x_<^+MTyDK6d)fGo}Uu#!*CU2yDo%YII-wlJ&=3JL6-&?~jHaNr?v>dcd?d zZ_6_AiVL&%J&heL{zxkRfQmsSVB@u!hQH6uFt_|-l>cZn1CvsI!7}i?FRS_=&M#tg sn87fLNC<%&w=PRFB#&lWlCteL=4+cp1QdLax`R3=p00i_>zopr0LE<`UjP6A literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/48.png b/simulator/worlds/sim_markers/48.png new file mode 100644 index 0000000000000000000000000000000000000000..160e6c8b508b2fc43e44f1d9ca690eba6e659da4 GIT binary patch literal 3264 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyyQhm|NX4AD*Dhurao}+{ zm{agy*IM++;{zUZ?)xxL+WXQ-MkL6}QVrnp8 z97QCAz=rH<#(%~AK97DyWuTzj<-TmrM9uX^pADSjzW&o=;1C`~M2Eoc zZQ>PIBxe@&`#dZ*e*a%U@ByZ69%OHdXb&A%k)UtrO9puCXLVFtq}A|V8B n?A&h7aBT;Jg3l--It1SE8|w=T$@D9lfVw80u6{1-oD!Mx zOrwaD5cvI#DgMQoo=D?lk77oL84RO{*bum}Z|?!|sO{nvmn3HzeK{HTb%vxKaS3)b zXTw9_HZc6I?A&``^0n?|dtM#i)5Xnjh-DNJ9RlCpvdV3BTQ+B+rgNH{@#o}wU(?fe zFevzpBBDbe=YCwn?rSHe7#pR&FkJtLslkA86p;`D8)~Z=xvrg=0S-Uo7zP2gQABhI zY|GdG(0c7H>yLK(&xYa*Eu5o>ln_{3T+RGIYBbzPN;ZEPuWk_)bgWw)0qUE0y85}S Ib4q9e02}f=P5=M^ literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/5.png b/simulator/worlds/sim_markers/5.png new file mode 100644 index 0000000000000000000000000000000000000000..9f5ffc6f8e488463e99fb048b98528c180a63532 GIT binary patch literal 3265 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyho_5UNX4AD*DmHBHV|Mv zXb|vU*O>Q+JR?iolDU};{$?9?Y~A0m>itW`12T*a2BV0$5ZLhgj4=>>kotW>TaST5 zcodNm0=t*7-^udle^8!X#UQ_fLBVGf5gh_=X2~5;&#pS~`Hk}WJ+Jx;869RYj3N?3 z;D)JfLu7T;fw|YdG&~;-K9W)oFzg;QUYnWzZDRPjnlm%=E3f*Ob#OBrVi`q5hrqg* zjN79wGtW!+=if2;|Nrwb3<7GSh?Ed0^X1=h)$Y?Nt52s^|2X+iTb!YVa}*IB0`Z@5MBTP literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/51.png b/simulator/worlds/sim_markers/51.png new file mode 100644 index 0000000000000000000000000000000000000000..d568e2b16aefc1eb9d12c52fe04ec7586079e9d2 GIT binary patch literal 3249 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyg{O;SNX4AD*BU#I81OJ0 z{Qme~^tY=59Af3xmm5Ub#nygi5Bck_&u~VNfkSu{5f=it%a5(GPJWO+M>{x%K|pO3 zkrD#imWx+h-D#8>JM&QSzyHa4#0B|i+JuC_?aSPIvSMczt@C-f)B6AaZzSZtOYLn6~CB02=Bs~W@CzGU6kuby4Z z=rDs}6p;`DH|E7X2u-hQGzW$tN5N?Dk&=4AVV7OS_(N~Bz9qFO5uIMeoY2F~aEN6T c5gh_{=aqy77Hxg}1=JVuboFyt=akR{08zj3egFUf literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/52.png b/simulator/worlds/sim_markers/52.png new file mode 100644 index 0000000000000000000000000000000000000000..0c1ee419131c5de3d73ea9c9549e65df678dcfd3 GIT binary patch literal 3270 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyucwP+NX4AD*DhwV8uBn4 zw21kyYt5@7&)*}Hmo7Eo9Z&bQe@v_P!X1Naf(T)MtrpdtUvGB`(xQlP4quzTJs?u)@?Rwckhbm&C1@(ZC}y z1i;~EYn0mS^RPJXS+X7jhwvyOIs~fo^b4-++IztB_ctc?y{{7Ejd z*1lW7GBC5(XXejTXMW?)r$@t(=+J|wG+;3PI4w7GliRX0HQe)y869RYj3Qz~;6`zE zPky42evmi0Bab{f#O9#hthZp0B?##XMyi&9|h5 zz=qoDM)$Q7HPcg0t(G(XEN1t7Gzf_+_aGGnIQ-_`=C*n1+b>yldY0s8H9ZCn;Za0% z2;AN-{=+Fd?TqA1qt7#DZvS8@&d|a+ibx58x4`t5)$3!a9NZ@NSHfYNX4AD*BU#I81OJ0 z{Qme~^f}W+ok?>u9lUtDuYG0@`RlLGa7K`ULwFPs7Xr7-kFBvzeqcOLJ2-|xKy4I} z5(3+ni(9;!8TVj`>D~js?^OQxx9#C(IK(oFhz@~oUzpt2zGQt6JDT`N&3mxKS9@)$ z=5)Dd=Z@EOjV3;l@*Xhp>2Fyk-Y{D7kyh}7QeIZ<%ug{hi(V$b+wn!WJnoq{|6`^G zBgO`UQABhIY_Q#XfE!r)`Bkm+`FTbEw}?1H3+E^zIt1RnWSy~fnYhKvVEN+q(Xx-E rvJa^En(bP0l+XkK&i*}6 literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/55.png b/simulator/worlds/sim_markers/55.png new file mode 100644 index 0000000000000000000000000000000000000000..728a1c4dfa22a460612ccc28db58ca60fedde17d GIT binary patch literal 3258 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyqo<2wNX4AD*DhwV8uBn4 zwAk@~+FY(rf);D`+@8s%a{u6>(tQo9-oIpRuw-IL97RNi!0&T2O4E7v9NTGhlyx-N zNDP7Msz&#@FInepStedFLH=|6P6h>^QABhIl|@&Mzl_7>hHsaE>CPL*VU8 zR=KU~d@RG4#e7ln2kHz$D1Pnq9?M_i!}pl2USkvu@O7Zkv~({SVK_ tFbJrPB2q$N+qo}n2P8%dJd(=$zYKqUgaxlNwQGVpC7!N+F6*2UngE>GJzW3* literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/56.png b/simulator/worlds/sim_markers/56.png new file mode 100644 index 0000000000000000000000000000000000000000..002528d58dde53ee6147809de0eb509d6158e8a7 GIT binary patch literal 3258 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyqo<2wNX4AD*DhwNI`S|$ z_TT&yET{h9w?&0V+HNhzOo@z=cj5tG_wHrb;Lf1nGm3}{fj2WHi_?xB$i6dCQ;&f| zcodNm0@d5Z6Ryk7EOJ}6=e41641<8$C?YxpzUAm2*bhv0=_#kqM7PO()*4NJq-MX- z^hb1R!_1kW^taB~=xLviWYw9~JBTYTN9z?x2)wx&*AQEta!PXMq5s8<4l@`=5eXr1 zWAokv?swm^+P%!t|KMo%Y<CPL*Ori^j%Sbb+;^@f;uIhu6{1-oD!Mj(R8zD}{Kq2Gy$td@7!-U)5z!%#!7z$Q2!R{( z;vTF%H&yfXsVU*LXXfTtUY#8Kh^fJVaTF070vl}i9tdCil6Btmn3+W{kKg$@BYZR+ zl9UaBiLk-{+EmTwr$qi~i!-!vjv`V*;O$FRxfeeAAACWX*!Je?(Mp7*Iuu-qWCJUa yk17ABj<#QjZ1cgDU7LB2LE`afl|f34xxxPPDsi5aq{;t4eG^YtKbLh*2~7amDH&7% literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/58.png b/simulator/worlds/sim_markers/58.png new file mode 100644 index 0000000000000000000000000000000000000000..e357a902c46fb824dd44fb2e9a907744c69d7b9d GIT binary patch literal 3252 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwywWo_?NX4AD*BWz=81OJ` z+;!rg_HJ*Mp#S|b4qc5hCGYqHzV6=3u)&={!Dkc^7Xoi)N*1RbJ8*r6a&QcTfZ8Y` zB?P|Z=znN@_l+svN;%l(X8lv91_Q=XL~ICbNUv_>FWDwuartgsL#Q4DhwvyOB?PL0 zL3dSlW|7;n0}SE}Eu5o>=n$~I&AsPUkIzo!;5NHsleg|9uB;rbR3IVn=4RZ3(DLfW z^N&y78m%-)tuw)u#ytc_BiC8zCsx_Vwt2RFkZmQh4>2%MWMX>7#0@A#SdFGs6f q5?dS_inp6HT-z~P;*k;pZ}=w~N^oA@n4Asjj(EEIxvXHfYNX4AD*AC{Y81gV2 zyzKaMxsLmT`HqQKLOCZ`A2Qo<&VIqG|8H3vs#zEgjUpmL;MkK}8=h0M%zpe!g^g2g3rAj2PjHAxZEp>%U8qGe2EXukPX=t#nBY zfp2eF?OvSei8M}*Gx}`gZ`(5(ghV!4Aj+=Ilr%PCy=VCOdkLV|o7ZC)1k^?m(IK!cU;o1jQ=_LnK9WYCXUvp8{CzY4NlHJU0BpVn z%*p3xN5(2-!G2JP3TefDR=KHEMJflS*Nkt&A{42P=Ywv-{alqnF ukAXvY6p<1F(a*oJ9WWgYHGKz=}fp2eF_g$DNxjF6Fgl9~n6$c3+u%WiP z(S7bj&Fqv@7n0NVywrXFXfyzct9Ie-6L1)=ov4`(4nv+=BfHT=2oD@$7@z>u-;$^Q zp|)nz|5Ky&FT7?TlJWY^J%$~}M#GKB;DRWt&zd2|_a||B9jJTa>FVdQ&MBb@0L1=* A8~^|S literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/61.png b/simulator/worlds/sim_markers/61.png new file mode 100644 index 0000000000000000000000000000000000000000..0b1ddece865b3b1ec038cdd3f7d119e95af402b3 GIT binary patch literal 3255 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyou`XqNX4AD*BUz?81OJ0 z{66_#^tY=80xEjemm8k2h|T@Yzv9>ay$l=N85Dd*5pf~#W~O9u+OY%acP47;F>nZv zB2q%2dYgE~RoR(EZp&&;o#~hSwDSLZrUnDXQABJAY)G$e44*quvpnV0b~)qZXOnBY zxET(yj3QD(VBTBSxKj6Jb0%uuuR6ms|IPIn1_8BEM05yz%hCU^YV+O$v(w~?-IwwF zyz+nk!_jO!T8hy)1VCk<{fj*P56d?1Wmr9$XGtouKzWvZ?pxLex}&WYQX6g7<<-my gy`%LkNzIHp2Gar&ekN|!UQmz3)78&qol`;+0KI@Wd;kCd literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/62.png b/simulator/worlds/sim_markers/62.png new file mode 100644 index 0000000000000000000000000000000000000000..a3d685738634a722bf908f69244544e609a7ea74 GIT binary patch literal 3269 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwykEe@cNX4AD*DiJ*G2n4H z`1#?#=x?C~^4yMTpVOo!JUZa~`VT|s{_Wxn?*th*ghvr^A#mF``PvzYie6)*qpS>x zOrwaD5cvI#sozRD_}Ymniu2Z-9SuK{k`E~GqLhQrotUz)f96c1lxOGnygC`fAfPsi zNC|;$`T8GPubr7;Y@BxF!erjwN?~z^7S2&bbO^k?&297QPTT{}?37byCiDKCF;l%- zbTkY}NAR1RKttY^kgLuY@pFNVgWnR&DKUNf8y!t=d#Wzp$Pz$vE1MQ literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/63.png b/simulator/worlds/sim_markers/63.png new file mode 100644 index 0000000000000000000000000000000000000000..49305f7e779207f0a0f0b95f69b19f04765ba4f7 GIT binary patch literal 3272 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyzo(01NX4AD*ADhRG2mf1 zc-86W@-^NM?3g94^zGJSykvCYft9_()%R~%8>(3t4viurL*Uz)8K%Z;agTd^3K<<{ zFpMG+Lg2=}y$2SbJ2A!7=xMCH@!`p}XIL2$nMM)OA@KVfQ~ZlFJ(0%A_l}H)9vLYI z6o46b;~xB;bDP`dW$6F=qY03tPy~iwf0S}CFb^vl`seJhM|yt)BNLG=@PyZ4?n50^9QSKddONX8d=) u|L5w_up>3;Y&-Xsw;_8p+(-(yZ~S?GMTLa7tY-&xQ9ND!T-G@yGywp1>=>f} literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/64.png b/simulator/worlds/sim_markers/64.png new file mode 100644 index 0000000000000000000000000000000000000000..ce48abfe4188497a3677d4ae11f30d510988eab5 GIT binary patch literal 3276 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcyIWKS2zkcv5PuU*V#QRHzr zC~&d<^pd7Ob9?&CFE^ZB_vJ}xUBjyTx49X@*&m$2~rUj1Dsx zMiB`iaAV)z1MYJtYG$XL3htj-d3wUa|MrJj84{UB5z!&=`y11r^j;rJ<>0h?Mqg$u z-@%~ZGm1zFft>qs4gcqWQ{kEDHo4Ct;>6|r(IOHO0&j0~*DNfqZhXHv?bw+=GZ!8| zQ!rW*l2Q?Xi@~)MHPcg0MfcA%{CslWBc=ue#!*B<2yCdWZq(nBr~je0&F-e;%uiST zmyaew5)&aq`C4FBz67j_AC9I!60_fi(!6|zn8%~xMoPGCu%EqLTBc!NX4AD*DiKC8}cw5 z{QU4=beYx(cZCyA-uf_4GSrEB&cEW<{=EzZ{R|E>MiFr#aATh2=Cor6lIKj+)MMZf z9z~>t!0qkgKUNtVrS|$rR-IYQ^Y_f;9SjOSqloAb$hjZ)VD+^VQ;dzC2KLXaJiS@Y z_%JI&BGV`$B?NwdV-lG&(=a=2H2o2s@*uesl>XS)&NNIA-BPI9i2}T7v?skOy;3jh^=S%&g>|U(7h#av>oEZtUB8fIkXY z2`_WEZR_7jV&)&MsB`AtmS(s<+GZgsCA?vsWFsapQ_gQ6sB_}!>gTe~DWM4f5TgwB literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/66.png b/simulator/worlds/sim_markers/66.png new file mode 100644 index 0000000000000000000000000000000000000000..920b781e3a02ade8a58d3d431f61da3a09f8c874 GIT binary patch literal 3261 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyi>HfYNX4AD*AC_$G2mf1 zs1fjASDRPGzWG#Tx|BjHGxxgBtRc1c;}~|hGbs3sBH}_I=X_73aqkxW z)NiF6eC@=Pa@(|HtL2Ojj|L#A=?55w@-NPSs$h@e(Y#Aa(FMx8i?6+9{V`YmbNo)? t8atz{LU;(|%)KqmaDB&UxRDY9Z`dcvi3nDT2~>jmC7!N+F6*2UngAn2Ik^A; literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/67.png b/simulator/worlds/sim_markers/67.png new file mode 100644 index 0000000000000000000000000000000000000000..ad6308853906ac980c021694c2089ef8386ef0f2 GIT binary patch literal 3276 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcyIWKS2zkcv5PuQhf)FyLW0 z`2F#}=xrrnhQNl}>PGjqx4HM6-?{g|Ny+l|&EeD((1XE?6h~d`u-c!0XfD7gHc3W2y94CJ2O*~d3~Y#vK{*wNXTL2z<-Y{}5PS-T2=4^T~53ME=d?W;nz$iii$@ zd2dmqCQ#SJ)78&qol`;+0K~g~ A#Q*>R literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/69.png b/simulator/worlds/sim_markers/69.png new file mode 100644 index 0000000000000000000000000000000000000000..3e88a2de4a581c6eb2f48a6cb7490733eeef66ab GIT binary patch literal 3268 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyx2KC^NX4AD*Dm(nG2n4H zs1fjA_qEiH8Ww{ivHNGTc|;#zzW$Rr1RaiZZ0okbeO>~iiiz?8}s5G1g2Lts_%W3{3Ae*fkSu{ zkrD#8FLQs;&#r1@pKIP*dHL`Th6O$h3Zsa~5XkY@|FEJo_0&$I*8kWk_Ti zMMQ@{bycJOmSy4?&MbPF_^#&6 z>K&t5n3NCz=VH^n4EsMD%s(}ngGnvGUi;)T^c0RZTS#fO-Qd6fUX1@q)Rh;Y?unb7jnM9rFWqoGGq;sFMo$efvm*=c9IV~9(sqgfgr0^9QSKeV1ZGsDz4 z?TB}NWj8m&A(l}@bO?NV%lhxCg7HAZJ+ug zecn$aJq8ZpQAA1z+})z4*}Q$iB}R5JZB literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/70.png b/simulator/worlds/sim_markers/70.png new file mode 100644 index 0000000000000000000000000000000000000000..256af9aa25a545cab3b206e66892c78efda04dbb GIT binary patch literal 3267 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwym#2$kNX4AD*ADjHFyLW0 zc-isq_BAFG{_q%nJ7?l>i%BfzIs1xVbK@9hC^HDCjUwVgVB7ni$ns+c(&uOg#}KDu zG-%-=ux+_`#lqtBGrNqRb$Rc9)2DCS!_9Dth2hXBA~FQNePQaiy3B3!^4#$?{JuZ0 z{l5Q@slkA86p;`D8*KL;SbS}&=JQii@@skiYl<_paE>CPL*VU8*0>B`{SQm5<%`$( z{JeJh&S(-ODF=emANSmstoPD=^$*zZU{LTGMWlp4j=%ng6~=oR>c6jSj|7&5Cs`R1 znMM)OAy8e_=)U$P>x0zopr0NHFr82|tP literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/71.png b/simulator/worlds/sim_markers/71.png new file mode 100644 index 0000000000000000000000000000000000000000..132b7093b3cedfbed9d0288f7ed894e71ea05e12 GIT binary patch literal 3264 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyyQhm|NX4AD*DiJ*G2n4H z`1#?#=x?C~^4tfLKBq}d5IXGq`VT|s{>$79#VibmMiG%A@NKT)>vIwny~U|r+@rxp zVhDWu!W93)NB_eT)4c~KzuR+Wb=}O-KqNBZL6iYQ@WTvX=zT9_beO>~ibx278}s5G z1U^4Cr8M=_(*BR9?aE#zzbhCGMN$$GFbEs|uh9-ZH&t``ou8@eA2Br;FpeT(LtumL zUIy0msz&v-FIne384WuUlMXQW4lKSlRTG$p!*_l*hx zOrwaD5cvI#sozRD_}Ymn<+W#c{J$BBGqiAyBBDd!?QQOwg~rBdy*`q^W=I#K}LQy@th5SRjOvhKz`@JvrRb!PI}8t(bUj1Dsx zMiH?gaAV(IhX0Yu!RJm)Q5?;{gTe~DWM4fIJkAz literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/73.png b/simulator/worlds/sim_markers/73.png new file mode 100644 index 0000000000000000000000000000000000000000..8a90fe7652146b9aed1709acebcf472fad9e93c9 GIT binary patch literal 3263 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyo2QFoNX4AD*DmHBHV|Mv zXb|vU*I4w);f4d7w;1z&ILRiq{TqM4S8H2_8OjU-YNLp_5Gb2lnKrkQEiPHU@+d1q zBGV`$B?PX2X^4LNlJWfJzCE8-**`nuU)I6RaEN6T5gh{SUNUZvy3G7;OZ*qE`=>@z zAgLJ;m;%{vfx>SYyTNGikyd_zbFcFHHOlKhSpNTiJchUubF^xKgg}`uf5ie@+Xt1` zzA*fI$kbrKIEqLJfeqPJ2i9Jj82)Wyc-&XR^}kNWjn=;;XJ2+u)$wIj|HEWG1`gp- oL`n!mS649_92>21Nvd({HzbJh@f^6z59*nCy85}Sb4q9e07o74-~a#s literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/74.png b/simulator/worlds/sim_markers/74.png new file mode 100644 index 0000000000000000000000000000000000000000..535aeb5faab0bd3e6e287f33d2016193e4be2516 GIT binary patch literal 3262 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwytEY=&NX4AD*AC_$G2mf1 zs8R4=S6lQ+;(-P4_89X{*wn~+{U771I)8nJIm!$IYNLp_5GXs_6KS0M!1UV*5pjkV z&QU~42*fTIf3T*us`341Km89Xqv1zV@&N^&eRawyshNkC7cn}_U>HRtguso`sz&#> z6E(9_xW51VzkMfzg3l--It1QW?LDyg+KDN)Mo(k=XBvJ!8F!SGA(3em5gh{8U$V+= zbzin-qUQUmGd%O(n2nZxB$a+Z#lQlv;)>MFP43I~yf&=S7!5#@0uUH_Kb&5FVe+rK un!MxeXznGo@XDTlkHO;DXtkA@B`Yd6eeqGyLx!4Lg#O4k+NZ=II~Et~(R| vbhOMPuCWKt+#5>s@)=?tkA@p5;kH3u^P7mk&+=vRpl*q$tDnm{r-UW|aLWTT literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/76.png b/simulator/worlds/sim_markers/76.png new file mode 100644 index 0000000000000000000000000000000000000000..10f9564ad2280b13b8d83368676dd2ac0c790b78 GIT binary patch literal 3275 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcyIBu^K|kcv5PuN~}tV!*?2 z@amzT%hz}(*fVoH>fNoy7-V+g!7Y1-tM9)s9gtycFc?L|g}{dA=Vp|q^Xxgc&*&&C zLn6~CA|(XQe_@KxxG%YRozKqCGpzF;7V9x^2#+G7L!f$@xJ8z~{(<{(Uz4wWVN&18 zpx`r#NC|;AGvgktFx`9Ld*xO2+n2d(YOf{>(o09pz%)PGL?7d~1CliCD{uC8KE=;3BK#4?JA4uOA+U57=44J{@v2K7=r MUHx3vIVCg!0OH0Z@&Et; literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/77.png b/simulator/worlds/sim_markers/77.png new file mode 100644 index 0000000000000000000000000000000000000000..2ce1a55ef3a3993ab322bca733af202d59c2b813 GIT binary patch literal 3270 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyucwP+NX4AD*DkJ9G2~%5 z`1#?V;BVat_F`<^bJuw}%rZ2IdM+REwZ4imp^w2~#wa2#1a9mzPP;3~yuNUqPa)%I zu#px5H|E7XV4rK9d~K@cbh~H1?`+Pf>M?K#k0PQ&;Pz#1n^!S2H?8w|SRMB)do=V& zO+3H={BUw++ylq#Dn`44(Hu-%sRyYZAldgN>yHQLPOs4p)*CGVNvbwM!55fa)u=vK zJ2>v^>A0_k8u zS#b@4_x_L8A0#(EqSLFG6MDEA4zY|PqC?;x!^Ec|f>L6gvY-x%r>mdKI;Vst0IPRs A`Tzg` literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/78.png b/simulator/worlds/sim_markers/78.png new file mode 100644 index 0000000000000000000000000000000000000000..64712f3ee54082e1d5230fa0cfe92f3381dfefc0 GIT binary patch literal 3264 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyyQhm|NX4AD*DiJ*G2n4H z`1#?#=x?C~^4tfLKBq}dm~+7S^&f`N{g=5Jidh&AjUpmL;M-io*XJZEdW%!LxJQGH z#1Q!Qg(?1pkNyGsYhRe|A7^DqWEw?8hd^~zqyCm<;tk7pFevzpB2q#i$6x=$isIB$ zyNsSrvY!7YH16yFSOx*LQABhIY+Ei~ab;HA1JCp-My42|wT%WbFa*G1x6>#!c4pDZ zxySGPG<^TdxO_AdlUjy>GjVC^shvhoL;H7r7PEWK)L_6kiiiz?4Yqp^h(}%Kws{%a y|1epPfkSu{krD#Y=~c`LJ)@02A{)vOW&ap%$_op{TI-vEx+b2kelF{r5}E)G!A9}` literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/79.png b/simulator/worlds/sim_markers/79.png new file mode 100644 index 0000000000000000000000000000000000000000..31016f370b5000ab6594870f8f181e33aecc27a0 GIT binary patch literal 3274 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcyIL{AsTkcv5PuQg^KFyLW0 zxGUxV)Vr!pcYeGVo6z#`<1KrKtM6a3HWafk92!MLhQPd;hS_O6G0FWtADJ5r7)KEa zA+X{37pC>MmW%(GXKIxCA~CIRXO}!f3+E^zIs|N&i&tEgnz_k+*`3leJu{QjJy$5Ee^+hTNudAA|`Uj)K42DreYzW*au4?3uGEV+>X2xuuy=Q#?Fevzp zBBDd!jpg10>~Db;Ny@1+hQ`~0S((S4fkSu{krD#ce)=CAv(wJZlQe#`Ec)Fec7{Zz zQABhIoPWt0w-p$K%U1V2od0RGDMMmQZo^jpd*p2c4 literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/8.png b/simulator/worlds/sim_markers/8.png new file mode 100644 index 0000000000000000000000000000000000000000..620b70c6e099f3d3ebf361859675f1ef94d0e551 GIT binary patch literal 3259 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwylc$SgNX4AD*AC_~8}cw5 z)Cl;mt1bGZe8HRBGub?TK4886lW|pDzCObpWd;GYQAAt_Y)e13W~Si>sq~aCZiYiF zqllCc`1Y3d$9c(_o7|RtSQ$fHc#kGbNC<4p*Z;7>*eJEvr*i5{e&Z*h_wTbZBr=U6 zQbOSOH>Uom?cx=e&Lp2ZA!0XmGzXJZfC01bz6(3|GT0m)O?o8dJW#r`+AjXXE<5Ga zh4z`1XIAfEQ1BT=q=Z1u{kR9*YiAlhcMXpFI+@ott!*^-l9YR|&6G4YV!fCAZ2n88 u1_Q=XL_!E`D9y`fhR_k;cgnB-2y6xET(y zj3QD(;M-f)e+!I_(|Ua*tIqVy^#3Lz&d|a+iii$@x3{@#UiJ7yDhId8eV(%X-+!Lb z03Nn<0{ zd&$q{zZ}iJq-0-UJ+YxQFP|ai@o2b_5^fvhb4$gzyie7qf;uLiu6{1-oD!M(tQo9-rweC_|C!5!a0hF3xT&Y4YSjD_8gO(`G~2( zfN>O&5CR)&s~g?t-sY~EXKM5`)IROmWZptXhZzi`h}aOgv2X8z#n(8Lg3q5)*okg?LE+Z?#vAH+D(dj3>?Cvi0BZw zytlgIFG2{^?L74Q2)f!)z4*} HQ$iB}%Y%dy literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/83.png b/simulator/worlds/sim_markers/83.png new file mode 100644 index 0000000000000000000000000000000000000000..4aee9c6880d8e52b53c4df5887889202024b897c GIT binary patch literal 3269 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwykEe@cNX4AD*AC_~8}cw5 z)Cl;mt1bE?@xX#dw`a0>%zemu{U_t9x_o_xJIV|KYNLp_5ZIP}Y|Tu=4-)AqUEBkA@J=j>%I#!B{#b*tl2_DeSPB|qx^HBX$rUnDXQA9!rY^befjI#umgbRRa&~tuH$7n@N zQaj|@Oi5!S)_KX#=D!?Gdn9JP4W)Vc3^9*K!;O@1+h9LET9SQVrQBjr|HRYP&t;uc GLK6VtQB!&V literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/84.png b/simulator/worlds/sim_markers/84.png new file mode 100644 index 0000000000000000000000000000000000000000..98d817d8bd66678e704ab55d7f8f8fc076640afa GIT binary patch literal 3264 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyyQhm|NX4AD*DmHhF%V!q z_}KC1av%2x_D#tvR|`G3ER}cW*8hf8|1UE)STZprjv^vM;QZY5Yje{d7=N1@9>XA@ zHi}3Ifp0$i6$@=`8v?Vd4tVM@a0rhgqC=p18T+0Kv*ZrwfBVw#y^zsi2E!;KAp~w1 z+dgOo2HoUqKy3HbuzUxDg3l--It1R#lqHfYNX4AD*AC_$G2mf1 zs1fjA*P8c<93zk6w{s>AUj3}szp{qZ`s*{C5oF*H9!123!0q&7YmAd0n66U}j$sf` z8%3mqz_#V$JFc4UJuvs$7bcb+3<^G@i0BZ=@z*a{U7B(Vh!z^I|Fy}uzfx43p@nl4 zkrD!LU$V+&`09VK+4p9D5u?KlhEYUp2;7(#*WeE<_O4CTZ2z&zzIZhBNJ%`v(0kCh z&NvwqdUD0bX6jdu=3bHuFJSJ?EA`bs@E%xy)I4BnFkl=-#D>5I+r0kb69&o*VO1~X#dR0(-RinuW#dKIK(oF zhz@~kGbN3USnnl2oBwjO^dqtG-%y&D&k*x?G~7rDw+;3w)smckF1E6u9*U={pUXO@ GgeCy1CF5TJ literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/87.png b/simulator/worlds/sim_markers/87.png new file mode 100644 index 0000000000000000000000000000000000000000..613581572f4dcdcc2e762085391a8ee92060c7ec GIT binary patch literal 3266 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyr>Bc!NX4AD*AC_$G2mf1 zs8R4=S6lSXV}|Cgy~ex~wl%O`|Hrth&R?HljxvLQ+9)C}1j^3#L>ebQF#UEyM4X|8 za}<#h0YIK2MCRDKJX6fd7SzUS57SmH{}(dq>f0%ddK z9yESCGvn(bP3O3;hQ=|Y!AD{U07J0B|J#Wvz+BuB|IKu?4k5J;saQ~2#c21zD887{ tVFtq}A|V8B+*)qV&{sGbZlole8}jaJMTF{)#hQXTC!Vf;F6*2UngF?T2vYz6 literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/88.png b/simulator/worlds/sim_markers/88.png new file mode 100644 index 0000000000000000000000000000000000000000..08db45e1b573c5f6f83a369535bb5835f90a46fa GIT binary patch literal 3269 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwykEe@cNX4AD*DmI=DDpTQ z6v+LrJDKYp|HIS!X0qk9e0j39zG2nnZvB2q%2dYSlx$+mkBIA&Kd+7&Q5%wQNr#D>5Pd7I-z$3k|NUqNCaL@a zXW(pL29EnGv2V&~8-~;-+_ic47$hE#wpvJOv~95OQxp~4bJ%MdsDI+=>gTe~DWM4f DzFNRo literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/89.png b/simulator/worlds/sim_markers/89.png new file mode 100644 index 0000000000000000000000000000000000000000..8c3a4b3df8c8a694d162b6ef0138e6c4631483fe GIT binary patch literal 3264 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyyQhm|NX4AD*AC_$G2mf1 zs1fjAS6lQ+dccPAWsE`l4mZ92!w|ZEyEwx;K?V-tQAAt_+%`_Wc1GeykFn8FR)$2T zQAA1z{QkzIUy^og%}hh(HoKcW`hN}d7&Jzc9`PY?d%JkWLQ~^sXL^24S{VP#JBC3( zZ4{9b0^9QSKRBhQospVp^m)e2?KQ6rjYk8Kq%;Hy!4<{Tjo%}IiSg2MIpf2l1tCfG z0I(G7k5UdkcVdeF&nxP)e@^EuWOSIpFp5YBfgAhwGO)h>#`I@2@sZXD0ad|U^7I+} rN7EdsS?-3_c5{Yxqv1wUxZRNF^brw^|5gwR>Y8}E`njxgN@xNAraTvq literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/9.png b/simulator/worlds/sim_markers/9.png new file mode 100644 index 0000000000000000000000000000000000000000..c8f5c565d5e46b42c4e8fb1c73bfd07417994123 GIT binary patch literal 3262 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwytEY=&NX4AD*DhwV8uBn4 zwAk@~+FZ6t{p{^BdFfIUYS_E4{bX9TFJGVGjxvLQ+9)C}1h%CgTQk$}gH(D-7dOKp zmQh4X2z-0X`s1|Z%uQ~~cs`!Ad&bmYz&MJC4S@}{)s60RCu(M=oVqY0yw+$m{D=)X zcq#)1A>+Ryw`FT4Y8vN1EY@S-5FSNDhrsRa;uTkH$2RTQ?0cW|Mgx$f^aBjQ zhU0UJS^Z|_nA55@4YyN9qtSYKBI`Z5Xd>-6KS0M!0_A&5pjkV z&QU~42)w<`U9-^GIIY)bX62bP(G~N#84j_GBBDd!+gsLs7iLKoyDj6fZL2@U%8zA z*FT`0o^ncRrqSnV3y<4$jTU{Rmi)ld&n~Oi$5J_1?(2-1+ihMO8jmJIlJelTb7y8u y)chc0T)%8I`H`CZuI=2*SkOBfZX|`L;yjmM*k1*8O*~!wT-G@yGywodjFaL3 literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/91.png b/simulator/worlds/sim_markers/91.png new file mode 100644 index 0000000000000000000000000000000000000000..ce51aa15792ba65665b69b796bf549376f71761a GIT binary patch literal 3261 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyi>HfYNX4AD*AC_$G2mf1 zs1fjA*P8c9af9RBy~ex~wl%O`|H>TlcU~NWg*$_S&nO};1ai*zL>ebQFkLrMQ;&f| zcodNm0=Jio@3?Ael-ld_u=w8ptMqdnTG{qTOI7bmFA@KGx z_noX>pPg>YcxL`gb&mULWE{gFpf-w#4uNg{`UR_Be_=ZRc~$&2Km89jqgj~LEWG2Y z)n11GpAGJxVr58V8bzdp!0#_i+jqGwTQgA;n0XCr&Wwg2NeKuPd{N8A8`29I9cD0$ tA`(L2#xB2n2ARi94F-&(h}aO=V6S*ygrBKn6Dz1+;_2$=vd$@?2>{FH;DZ1F literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/92.png b/simulator/worlds/sim_markers/92.png new file mode 100644 index 0000000000000000000000000000000000000000..ac0bd5732abd5716a178ae0eb2643caf58abad2f GIT binary patch literal 3279 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcyIG*1`Dkcv5PuU*V!HRNG9 zXc6;Y*IM+6a>0qpdo$To?jKyVb$`RE_m{aDo^vp?aE>D4LcrEI`PvzY9mnKmK4NMx zU>rpxgusU9Uzpb4QVza$V#;bc?*aB(z+CJa+-7(4%#4}-W=B{V5}8I3(IIgDC2QPP_hoA)YKGfB zTjgISI+}?|DxbiaSZd}b_hm6(XUyF0)4Q{io8b`4C?X{U=3VBlSzv0McD`rl$usKG zzy2G?FbJrPBBDd!+nl(D`)h!qC^!Gj`O&Z=HR){o_L8?Dc{JQe3b${}>rYDx&0zU6 Q3Di^ZboFyt=akR{0MlYQu9lDsiul;0R^>4X2!!r(s7S2&bTnN0iPQEtR@Pp*@QzGKT=@<=K zcnG|G$tqX6OuS-#Y3iw^^2N`S?T)fCBr=U6qC=p%s!=@ZGPljkn3@t3KN78u~H^U*8QABhI zeEY)GZw1W2nZEiT7Wn_2;a|w;FoR(fkq`nm=EXf|z4nF49~h1Xr$#FflIl-j1>(N; zCF{N8leg|w^}`!b*TmD+&t;ucLK6TM Cx;&o% literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/94.png b/simulator/worlds/sim_markers/94.png new file mode 100644 index 0000000000000000000000000000000000000000..331cb70e4b8ade71f1c391e5feb7d427a1083996 GIT binary patch literal 3267 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwym#2$kNX4AD*BUbq81OJ0 z+*R>^>Rr{Qt54pGO_+0FacNz{s{1cl8!VX^5=Rk{A@KX$jM8+TnqxbSj+!R42DreLI~WL z7xy6W`Kc+Tsi(Hf6(3(NSKQ9caEN6T5gh{GzA*JiUFNoVc`o_fRL%Ln4Cg;$YA|3N zMZ|`{2HU*{xPfKhz30L6bA0tbsEihaq}Bq!l2ER6olj)=vYOY1e}eTGID|(L(IIdf zn0vE;32%wNZCm}R(KZZ`?LN4&*FO0SJ%yuHFDWgL8}f=QqJpn=Q)@uI6Hiw^mvv4F FO#p)vlkA@J=j>yPu2GdH;{wj=cPdhV9(m3hG$%-?q42evmi0Bab{f+5PsoSzO6E&UXKZlMcKaz4EDD4HNr<{_S zd8qi`nPffUYMRk186E<+w~JRSG&W8<+aqcCCB=B(%i}v37L3*%#D@T|?s%~H+FRCp z$0u(ct#*kIH;BrdxwoYmuJ0harY6<=VX6KN|GSr>d{-CDV+VCgJYD@<);T3K0RUbF B#_9k7 literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/96.png b/simulator/worlds/sim_markers/96.png new file mode 100644 index 0000000000000000000000000000000000000000..75b4988ce3ae8370b1f39682aac709a590c46db8 GIT binary patch literal 3264 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyyQhm|NX4AD*DiKC8}cw5 z{QU4=^taFg`NsA+Dd|!ZHY8rOvUj-p{w-@mH4DR`QAA`2d^LWi zM-d4ju%WiPk-sEQ|HBefqob4eYKt?paE>CPL*VUgZktykR6x? zf7r}>6T6e>mdKI;Vst0OP`O A1poj5 literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/97.png b/simulator/worlds/sim_markers/97.png new file mode 100644 index 0000000000000000000000000000000000000000..dd8f5cc9896ad141b559e01540789793837be651 GIT binary patch literal 3269 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwykEe@cNX4AD*BVzoG2n4H z`1#?#=x^&26;G(-FJnA8`{}~ce~h93^Yt0-C^HDCjUwVgU|agJH8TxANT#QBaWfoZ z8AYUoz_+)oa;0v|)=boNPOICbIRBTCaSU<6KAJuuA+Rl9|HBGXqo+MS56g|?)1Jkf z9bsiiWEw@Jguw4_O!`~Wj?I~ZU>S3j3^P6yqe5wo7Tq7aEN6T5gh{G-m=PVbz3$EmHn@mvv4FO#ri9zM=pC literal 0 HcmV?d00001 diff --git a/simulator/worlds/sim_markers/99.png b/simulator/worlds/sim_markers/99.png new file mode 100644 index 0000000000000000000000000000000000000000..ca417e652e08396914c78559ea05d3dbe5ad369a GIT binary patch literal 3255 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&w3=LsVHwJOcwyou`XqNX4AD*ADu!8uBMwXzUB#Ht$KWty6cHB!H})B)-IZisU%1ZaBjaeW zkro0s=EXf=o@g3!GHRf7@yCj6Mmo`iKnZd%Q~loCIFBB*xvvE literal 0 HcmV?d00001 diff --git a/zone_0/robot.py b/zone_0/robot.py new file mode 100644 index 0000000..6e791eb --- /dev/null +++ b/zone_0/robot.py @@ -0,0 +1,26 @@ +from sr.robot3 import Robot + +robot = Robot() + +robot.motor_board.motors[0].power = 1 +robot.motor_board.motors[1].power = 1 + +# measure the distance of the right ultrasound sensor +# pin 6 is the trigger pin, pin 7 is the echo pin +distance = robot.arduino.ultrasound_measure(6, 7) +print(f"Right ultrasound distance: {distance / 1000} meters") + +# motor board, channel 0 to half power forward +robot.motor_board.motors[0].power = 0.5 + +# motor board, channel 1 to half power forward, +robot.motor_board.motors[1].power = 0.5 +# minimal time has passed at this point, +# so the robot will appear to move forward instead of turning + +# sleep for 2 second +robot.sleep(2) + +# stop both motors +robot.motor_board.motors[0].power = 0 +robot.motor_board.motors[1].power = 0