Compare commits
	
		
			9 Commits
		
	
	
		
			8b7927a3c5
			...
			main
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 9f86d060f0 | |||
| e2f4a0d2a5 | |||
| bc96cea2b9 | |||
| 8d35f76b56 | |||
| 97f9765705 | |||
| fa61e27825 | |||
| b60a0aba4f | |||
| ae02ddefb0 | |||
| f1fadd123f | 
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -10,3 +10,6 @@ wheels/ | |||||||
| .venv | .venv | ||||||
|  |  | ||||||
| .vscode | .vscode | ||||||
|  |  | ||||||
|  | records | ||||||
|  | *.rec | ||||||
							
								
								
									
										59
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										59
									
								
								README.md
									
									
									
									
									
								
							| @@ -0,0 +1,59 @@ | |||||||
|  | <p align="center"> | ||||||
|  |     <img src="logo.png" width="300"> | ||||||
|  | </p> | ||||||
|  |  | ||||||
|  | # Rally Racer | ||||||
|  |  | ||||||
|  | This repository holds a sandbox driving simulation controllable via a network interface as a machine learning and data collection challenge. | ||||||
|  |  | ||||||
|  | # Installation | ||||||
|  | From the root of the repository, run | ||||||
|  | ```sh | ||||||
|  | uv sync | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | To run the game, you can use | ||||||
|  | ```sh | ||||||
|  | uv run main.py | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | # Generality | ||||||
|  | Launching [`main.py`](main.py) starts a race with a single car on the provided track.  | ||||||
|  | This track can be controlled either by keyboard (*WASD*) or by a socket interface.  | ||||||
|  | An example of such interface is included in the code in [*`scripts/recorder.py`*](scripts/recorder.py). To run it, simply use the following command: | ||||||
|  | ```sh | ||||||
|  | uv run -m scripts.recorder | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | # Sensing | ||||||
|  | The car sensing is available in two commodities: **raycasts** and **images**. These sensing snapshots are sent at 10 Hertz (i.e. 10 times a second). Due to this fact, correct reception of snapshot messages has to be done regularly. | ||||||
|  |  | ||||||
|  | # Communication protocol | ||||||
|  |  | ||||||
|  | A remote controller can be impemented using TCP socket connecting on localhost on port 5000. | ||||||
|  | Different commands can be issued to the race simulation to control the car. | ||||||
|  |  | ||||||
|  | These commands are declared in [`src/command.py`](src/command.py) | ||||||
|  |  | ||||||
|  | ##  Car controls | ||||||
|  | ```python | ||||||
|  | ControlCommand(control: CarControl, active: bool) | ||||||
|  | ``` | ||||||
|  | To simulate key press and control the car. | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # Controls | ||||||
|  |  | ||||||
|  | - <kbd>W</kbd> Move forward | ||||||
|  | - <kbd>S</kbd> Brake / move backward | ||||||
|  | - <kbd>A</kbd> Turn left | ||||||
|  | - <kbd>D</kbd> Turn right | ||||||
|  | - <kbd>F</kbd> Toggle FPS indicator | ||||||
|  | - <kbd>V</kbd> Toggle speedometer | ||||||
|  | - <kbd>R</kbd> Reset car | ||||||
|  | - <kbd>C</kbd> Toggle raycasts visibility | ||||||
|  | - <kbd>Esc</kbd> Quit | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # Credits | ||||||
|  | This project is based on the repository [https://github.com/ISC-HEI/RallyRobotPilot_2025](https://github.com/ISC-HEI/RallyRobotPilot_2025), which is in turn based on [https://github.com/mandaw2014/Rally](https://github.com/mandaw2014/Rally) | ||||||
							
								
								
									
										143
									
								
								car.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								car.svg
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,143 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||||||
|  | <!-- Created with Inkscape (http://www.inkscape.org/) --> | ||||||
|  |  | ||||||
|  | <svg | ||||||
|  |    width="64" | ||||||
|  |    height="64" | ||||||
|  |    viewBox="0 0 64 64.000003" | ||||||
|  |    version="1.1" | ||||||
|  |    id="svg1" | ||||||
|  |    inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)" | ||||||
|  |    sodipodi:docname="car.svg" | ||||||
|  |    xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" | ||||||
|  |    xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" | ||||||
|  |    xmlns="http://www.w3.org/2000/svg" | ||||||
|  |    xmlns:svg="http://www.w3.org/2000/svg"> | ||||||
|  |   <sodipodi:namedview | ||||||
|  |      id="namedview1" | ||||||
|  |      pagecolor="#ffffff" | ||||||
|  |      bordercolor="#000000" | ||||||
|  |      borderopacity="0.25" | ||||||
|  |      inkscape:showpageshadow="2" | ||||||
|  |      inkscape:pageopacity="0.0" | ||||||
|  |      inkscape:pagecheckerboard="0" | ||||||
|  |      inkscape:deskcolor="#d1d1d1" | ||||||
|  |      inkscape:document-units="mm" | ||||||
|  |      showgrid="true" | ||||||
|  |      inkscape:zoom="11.313709" | ||||||
|  |      inkscape:cx="42.470601" | ||||||
|  |      inkscape:cy="34.957591" | ||||||
|  |      inkscape:window-width="1920" | ||||||
|  |      inkscape:window-height="1016" | ||||||
|  |      inkscape:window-x="0" | ||||||
|  |      inkscape:window-y="27" | ||||||
|  |      inkscape:window-maximized="1" | ||||||
|  |      inkscape:current-layer="layer1"> | ||||||
|  |     <inkscape:grid | ||||||
|  |        id="grid1" | ||||||
|  |        units="px" | ||||||
|  |        originx="0" | ||||||
|  |        originy="0" | ||||||
|  |        spacingx="1" | ||||||
|  |        spacingy="1" | ||||||
|  |        empcolor="#0099e5" | ||||||
|  |        empopacity="0.30196078" | ||||||
|  |        color="#0099e5" | ||||||
|  |        opacity="0.14901961" | ||||||
|  |        empspacing="8" | ||||||
|  |        enabled="true" | ||||||
|  |        visible="true" /> | ||||||
|  |   </sodipodi:namedview> | ||||||
|  |   <defs | ||||||
|  |      id="defs1"> | ||||||
|  |     <inkscape:path-effect | ||||||
|  |        effect="fillet_chamfer" | ||||||
|  |        id="path-effect3" | ||||||
|  |        is_visible="true" | ||||||
|  |        lpeversion="1" | ||||||
|  |        nodesatellites_param="F,0,0,1,0,1.0000002,0,1 @ F,0,1,1,0,1.0000002,0,1 @ F,0,1,1,0,1.0000002,0,1 @ F,0,1,1,0,1.0000002,0,1" | ||||||
|  |        radius="0" | ||||||
|  |        unit="px" | ||||||
|  |        method="auto" | ||||||
|  |        mode="F" | ||||||
|  |        chamfer_steps="1" | ||||||
|  |        flexible="false" | ||||||
|  |        use_knot_distance="true" | ||||||
|  |        apply_no_radius="true" | ||||||
|  |        apply_with_radius="true" | ||||||
|  |        only_selected="false" | ||||||
|  |        hide_knots="false" /> | ||||||
|  |     <inkscape:path-effect | ||||||
|  |        effect="fillet_chamfer" | ||||||
|  |        id="path-effect2" | ||||||
|  |        is_visible="true" | ||||||
|  |        lpeversion="1" | ||||||
|  |        nodesatellites_param="F,0,1,1,0,1.0000001,0,1 @ F,0,1,1,0,1.0000001,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1.0000001,0,1 @ F,0,1,1,0,1.0000001,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1" | ||||||
|  |        radius="0" | ||||||
|  |        unit="px" | ||||||
|  |        method="auto" | ||||||
|  |        mode="F" | ||||||
|  |        chamfer_steps="1" | ||||||
|  |        flexible="false" | ||||||
|  |        use_knot_distance="true" | ||||||
|  |        apply_no_radius="true" | ||||||
|  |        apply_with_radius="true" | ||||||
|  |        only_selected="false" | ||||||
|  |        hide_knots="false" /> | ||||||
|  |   </defs> | ||||||
|  |   <g | ||||||
|  |      inkscape:label="Calque 1" | ||||||
|  |      inkscape:groupmode="layer" | ||||||
|  |      id="layer1"> | ||||||
|  |     <g | ||||||
|  |        id="g5" | ||||||
|  |        inkscape:label="car" | ||||||
|  |        transform="translate(-5.9999998,-7.9999997)"> | ||||||
|  |       <g | ||||||
|  |          id="g4" | ||||||
|  |          inkscape:label="wheels"> | ||||||
|  |         <path | ||||||
|  |            style="fill:#000000;stroke-linecap:round;stroke-linejoin:round" | ||||||
|  |            d="m 25.000001,31.999999 v -2 h 4 v 2 z" | ||||||
|  |            id="path2" | ||||||
|  |            sodipodi:nodetypes="ccccc" /> | ||||||
|  |         <path | ||||||
|  |            style="fill:#000000;stroke-linecap:round;stroke-linejoin:round" | ||||||
|  |            d="m 43.000001,31.999999 v -2 h 4 v 2 z" | ||||||
|  |            id="path2-3" | ||||||
|  |            sodipodi:nodetypes="ccccc" /> | ||||||
|  |         <path | ||||||
|  |            style="fill:#000000;stroke-linecap:round;stroke-linejoin:round" | ||||||
|  |            d="m 25.000001,49.999999 v -2 h 4 v 2 z" | ||||||
|  |            id="path2-1" | ||||||
|  |            sodipodi:nodetypes="ccccc" /> | ||||||
|  |         <path | ||||||
|  |            style="fill:#000000;stroke-linecap:round;stroke-linejoin:round" | ||||||
|  |            d="m 43.000001,49.999999 v -2 h 4 v 2 z" | ||||||
|  |            id="path2-3-2" | ||||||
|  |            sodipodi:nodetypes="ccccc" /> | ||||||
|  |       </g> | ||||||
|  |       <path | ||||||
|  |          style="fill:#e14324;fill-opacity:1;stroke-linecap:round;stroke-linejoin:round" | ||||||
|  |          d="m 16,33 v 13.999999 a 1.1327823,1.1327823 48.562508 0 0 0.992278,1.124035 l 7.007723,0.875965 h 24 l 11.003454,-0.916955 a 1.0867997,1.0867997 132.61818 0 0 0.996546,-1.083045 v -14 A 1.0867996,1.0867996 47.381818 0 0 59.003455,31.916954 L 48.000001,31 h -24 l -7.007723,0.875965 A 1.1327823,1.1327823 131.43749 0 0 16,33 Z" | ||||||
|  |          id="path1" | ||||||
|  |          inkscape:path-effect="#path-effect2" | ||||||
|  |          inkscape:original-d="m 16,32 v 15.999999 l 8.000001,1 h 24 l 12,-1 v -16 L 48.000001,31 h -24 z" | ||||||
|  |          inkscape:label="body" /> | ||||||
|  |       <path | ||||||
|  |          style="fill:#53170b;fill-opacity:1;stroke-linecap:round;stroke-linejoin:round" | ||||||
|  |          d="m 50.000001,33.500001 v 13 a 1.0867994,1.0867994 132.61819 0 1 -0.996546,1.083045 l -4.006908,0.333908 A 0.92013337,0.92013337 42.618189 0 1 44.000001,46.999999 V 33 a 0.92013291,0.92013291 137.38183 0 1 0.996546,-0.916954 l 4.006908,0.333909 a 1.0867999,1.0867999 47.381826 0 1 0.996546,1.083046 z" | ||||||
|  |          id="path3" | ||||||
|  |          inkscape:path-effect="#path-effect3" | ||||||
|  |          inkscape:original-d="m 50.000001,32.500001 v 15 l -6,0.499998 V 32 Z" | ||||||
|  |          sodipodi:nodetypes="ccccc" | ||||||
|  |          inkscape:label="windshield" /> | ||||||
|  |       <path | ||||||
|  |          style="fill:#af3116;fill-opacity:1;stroke-linecap:round;stroke-linejoin:round" | ||||||
|  |          d="m 29.000001,46.999999 c -4,0 -8.000001,0 -11.000001,-1 v -12 c 3,-1 7.000001,-1 11.000001,-1 z" | ||||||
|  |          id="path4" | ||||||
|  |          sodipodi:nodetypes="ccccc" | ||||||
|  |          inkscape:label="back_window" /> | ||||||
|  |     </g> | ||||||
|  |   </g> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 5.2 KiB | 
							
								
								
									
										202
									
								
								logo.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										202
									
								
								logo.svg
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,202 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||||||
|  | <!-- Created with Inkscape (http://www.inkscape.org/) --> | ||||||
|  |  | ||||||
|  | <svg | ||||||
|  |    width="64" | ||||||
|  |    height="64" | ||||||
|  |    viewBox="0 0 64 64.000003" | ||||||
|  |    version="1.1" | ||||||
|  |    id="svg1" | ||||||
|  |    inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)" | ||||||
|  |    sodipodi:docname="logo.svg" | ||||||
|  |    inkscape:export-filename="logo.png" | ||||||
|  |    inkscape:export-xdpi="768" | ||||||
|  |    inkscape:export-ydpi="768" | ||||||
|  |    xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" | ||||||
|  |    xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" | ||||||
|  |    xmlns="http://www.w3.org/2000/svg" | ||||||
|  |    xmlns:svg="http://www.w3.org/2000/svg"> | ||||||
|  |   <sodipodi:namedview | ||||||
|  |      id="namedview1" | ||||||
|  |      pagecolor="#ffffff" | ||||||
|  |      bordercolor="#000000" | ||||||
|  |      borderopacity="0.25" | ||||||
|  |      inkscape:showpageshadow="2" | ||||||
|  |      inkscape:pageopacity="0.0" | ||||||
|  |      inkscape:pagecheckerboard="0" | ||||||
|  |      inkscape:deskcolor="#d1d1d1" | ||||||
|  |      inkscape:document-units="mm" | ||||||
|  |      showgrid="true" | ||||||
|  |      inkscape:zoom="11.313709" | ||||||
|  |      inkscape:cx="31.952386" | ||||||
|  |      inkscape:cy="30.803338" | ||||||
|  |      inkscape:window-width="1920" | ||||||
|  |      inkscape:window-height="1016" | ||||||
|  |      inkscape:window-x="0" | ||||||
|  |      inkscape:window-y="27" | ||||||
|  |      inkscape:window-maximized="1" | ||||||
|  |      inkscape:current-layer="layer1" | ||||||
|  |      inkscape:export-bgcolor="#ffffffff"> | ||||||
|  |     <inkscape:grid | ||||||
|  |        id="grid1" | ||||||
|  |        units="px" | ||||||
|  |        originx="0" | ||||||
|  |        originy="0" | ||||||
|  |        spacingx="1" | ||||||
|  |        spacingy="1" | ||||||
|  |        empcolor="#0099e5" | ||||||
|  |        empopacity="0.30196078" | ||||||
|  |        color="#0099e5" | ||||||
|  |        opacity="0.14901961" | ||||||
|  |        empspacing="8" | ||||||
|  |        enabled="true" | ||||||
|  |        visible="true" /> | ||||||
|  |   </sodipodi:namedview> | ||||||
|  |   <defs | ||||||
|  |      id="defs1"> | ||||||
|  |     <inkscape:path-effect | ||||||
|  |        effect="fillet_chamfer" | ||||||
|  |        id="path-effect3" | ||||||
|  |        is_visible="true" | ||||||
|  |        lpeversion="1" | ||||||
|  |        nodesatellites_param="F,0,0,1,0,1.0000002,0,1 @ F,0,1,1,0,1.0000002,0,1 @ F,0,1,1,0,1.0000002,0,1 @ F,0,1,1,0,1.0000002,0,1" | ||||||
|  |        radius="0" | ||||||
|  |        unit="px" | ||||||
|  |        method="auto" | ||||||
|  |        mode="F" | ||||||
|  |        chamfer_steps="1" | ||||||
|  |        flexible="false" | ||||||
|  |        use_knot_distance="true" | ||||||
|  |        apply_no_radius="true" | ||||||
|  |        apply_with_radius="true" | ||||||
|  |        only_selected="false" | ||||||
|  |        hide_knots="false" /> | ||||||
|  |     <inkscape:path-effect | ||||||
|  |        effect="fillet_chamfer" | ||||||
|  |        id="path-effect2" | ||||||
|  |        is_visible="true" | ||||||
|  |        lpeversion="1" | ||||||
|  |        nodesatellites_param="F,0,1,1,0,1.0000001,0,1 @ F,0,1,1,0,1.0000001,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1.0000001,0,1 @ F,0,1,1,0,1.0000001,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1" | ||||||
|  |        radius="0" | ||||||
|  |        unit="px" | ||||||
|  |        method="auto" | ||||||
|  |        mode="F" | ||||||
|  |        chamfer_steps="1" | ||||||
|  |        flexible="false" | ||||||
|  |        use_knot_distance="true" | ||||||
|  |        apply_no_radius="true" | ||||||
|  |        apply_with_radius="true" | ||||||
|  |        only_selected="false" | ||||||
|  |        hide_knots="false" /> | ||||||
|  |   </defs> | ||||||
|  |   <g | ||||||
|  |      inkscape:label="Calque 1" | ||||||
|  |      inkscape:groupmode="layer" | ||||||
|  |      id="layer1"> | ||||||
|  |     <g | ||||||
|  |        id="g14" | ||||||
|  |        transform="matrix(1.12,0,0,1.12,-6.6400002,3.1600025)"> | ||||||
|  |       <path | ||||||
|  |          id="path13" | ||||||
|  |          style="fill:none;fill-opacity:1;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1" | ||||||
|  |          d="m 38,43.499998 -11,-21 m 0,0 -9,20 M 50,32.749996 38,43.499998 M 53,16.749997 50,32.749996 M 40.000001,7.9999998 27,22.499998 M 16,7.9999998 27,22.499998" | ||||||
|  |          sodipodi:nodetypes="cccccccccccc" /> | ||||||
|  |       <g | ||||||
|  |          id="g13"> | ||||||
|  |         <circle | ||||||
|  |            style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1" | ||||||
|  |            id="path5" | ||||||
|  |            cx="16" | ||||||
|  |            cy="8" | ||||||
|  |            r="3" /> | ||||||
|  |         <circle | ||||||
|  |            style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1" | ||||||
|  |            id="path5-3" | ||||||
|  |            cx="40" | ||||||
|  |            cy="7.9999995" | ||||||
|  |            r="3" /> | ||||||
|  |         <circle | ||||||
|  |            style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1" | ||||||
|  |            id="path5-1" | ||||||
|  |            cx="27" | ||||||
|  |            cy="22.499998" | ||||||
|  |            r="3" /> | ||||||
|  |         <circle | ||||||
|  |            style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1" | ||||||
|  |            id="path5-6" | ||||||
|  |            cx="38" | ||||||
|  |            cy="43.499996" | ||||||
|  |            r="3" /> | ||||||
|  |         <circle | ||||||
|  |            style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1" | ||||||
|  |            id="path5-18" | ||||||
|  |            cx="18" | ||||||
|  |            cy="42.499996" | ||||||
|  |            r="3" /> | ||||||
|  |         <circle | ||||||
|  |            style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1" | ||||||
|  |            id="path5-2" | ||||||
|  |            cx="50" | ||||||
|  |            cy="32.749996" | ||||||
|  |            r="3" /> | ||||||
|  |         <circle | ||||||
|  |            style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1" | ||||||
|  |            id="path5-22" | ||||||
|  |            cx="53" | ||||||
|  |            cy="16.749996" | ||||||
|  |            r="3" /> | ||||||
|  |       </g> | ||||||
|  |       <g | ||||||
|  |          id="g5" | ||||||
|  |          inkscape:label="car" | ||||||
|  |          transform="matrix(0.1163734,0.24290774,-0.24290774,0.1163734,38.296019,19.616451)" | ||||||
|  |          style="display:inline"> | ||||||
|  |         <g | ||||||
|  |            id="g4" | ||||||
|  |            inkscape:label="wheels"> | ||||||
|  |           <path | ||||||
|  |              style="fill:#000000;stroke-linecap:round;stroke-linejoin:round" | ||||||
|  |              d="m 25.000001,31.999999 v -2 h 4 v 2 z" | ||||||
|  |              id="path2" | ||||||
|  |              sodipodi:nodetypes="ccccc" /> | ||||||
|  |           <path | ||||||
|  |              style="fill:#000000;stroke-linecap:round;stroke-linejoin:round" | ||||||
|  |              d="m 43.000001,31.999999 v -2 h 4 v 2 z" | ||||||
|  |              id="path2-3" | ||||||
|  |              sodipodi:nodetypes="ccccc" /> | ||||||
|  |           <path | ||||||
|  |              style="fill:#000000;stroke-linecap:round;stroke-linejoin:round" | ||||||
|  |              d="m 25.000001,49.999999 v -2 h 4 v 2 z" | ||||||
|  |              id="path2-1" | ||||||
|  |              sodipodi:nodetypes="ccccc" /> | ||||||
|  |           <path | ||||||
|  |              style="fill:#000000;stroke-linecap:round;stroke-linejoin:round" | ||||||
|  |              d="m 43.000001,49.999999 v -2 h 4 v 2 z" | ||||||
|  |              id="path2-3-2" | ||||||
|  |              sodipodi:nodetypes="ccccc" /> | ||||||
|  |         </g> | ||||||
|  |         <path | ||||||
|  |            style="fill:#e14324;fill-opacity:1;stroke-linecap:round;stroke-linejoin:round" | ||||||
|  |            d="m 16,33 v 13.999999 a 1.1327823,1.1327823 48.562508 0 0 0.992278,1.124035 l 7.007723,0.875965 h 24 l 11.003454,-0.916955 a 1.0867997,1.0867997 132.61818 0 0 0.996546,-1.083045 v -14 A 1.0867996,1.0867996 47.381818 0 0 59.003455,31.916954 L 48.000001,31 h -24 l -7.007723,0.875965 A 1.1327823,1.1327823 131.43749 0 0 16,33 Z" | ||||||
|  |            id="path1" | ||||||
|  |            inkscape:path-effect="#path-effect2" | ||||||
|  |            inkscape:original-d="m 16,32 v 15.999999 l 8.000001,1 h 24 l 12,-1 v -16 L 48.000001,31 h -24 z" | ||||||
|  |            inkscape:label="body" /> | ||||||
|  |         <path | ||||||
|  |            style="fill:#53170b;fill-opacity:1;stroke-linecap:round;stroke-linejoin:round" | ||||||
|  |            d="m 50.000001,33.500001 v 13 a 1.0867994,1.0867994 132.61819 0 1 -0.996546,1.083045 l -4.006908,0.333908 A 0.92013337,0.92013337 42.618189 0 1 44.000001,46.999999 V 33 a 0.92013291,0.92013291 137.38183 0 1 0.996546,-0.916954 l 4.006908,0.333909 a 1.0867999,1.0867999 47.381826 0 1 0.996546,1.083046 z" | ||||||
|  |            id="path3" | ||||||
|  |            inkscape:path-effect="#path-effect3" | ||||||
|  |            inkscape:original-d="m 50.000001,32.500001 v 15 l -6,0.499998 V 32 Z" | ||||||
|  |            sodipodi:nodetypes="ccccc" | ||||||
|  |            inkscape:label="windshield" /> | ||||||
|  |         <path | ||||||
|  |            style="fill:#af3116;fill-opacity:1;stroke-linecap:round;stroke-linejoin:round" | ||||||
|  |            d="m 29.000001,46.999999 c -4,0 -8.000001,0 -11.000001,-1 v -12 c 3,-1 7.000001,-1 11.000001,-1 z" | ||||||
|  |            id="path4" | ||||||
|  |            sodipodi:nodetypes="ccccc" | ||||||
|  |            inkscape:label="back_window" /> | ||||||
|  |       </g> | ||||||
|  |     </g> | ||||||
|  |   </g> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 7.5 KiB | 
							
								
								
									
										41
									
								
								scripts/example_bot.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								scripts/example_bot.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | |||||||
|  | from PyQt6.QtWidgets import QApplication | ||||||
|  |  | ||||||
|  | from src.bot import Bot | ||||||
|  | from src.command import CarControl | ||||||
|  | from src.recorder import RecorderWindow | ||||||
|  | from src.snapshot import Snapshot | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ExampleBot(Bot): | ||||||
|  |     def nn_infer(self, snapshot: Snapshot) -> list[tuple[CarControl, bool]]: | ||||||
|  |         #   Do smart NN inference here | ||||||
|  |         return [(CarControl.FORWARD, True)] | ||||||
|  |  | ||||||
|  |     def on_snapshot_received(self, snapshot: Snapshot): | ||||||
|  |         controls: list[tuple[CarControl, bool]] = self.nn_infer(snapshot) | ||||||
|  |         for control, active in controls: | ||||||
|  |             self.recorder.on_car_controlled(control, active) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def main(): | ||||||
|  |     import sys | ||||||
|  |  | ||||||
|  |     def except_hook(cls, exception, traceback): | ||||||
|  |         sys.__excepthook__(cls, exception, traceback) | ||||||
|  |  | ||||||
|  |     sys.excepthook = except_hook | ||||||
|  |  | ||||||
|  |     app: QApplication = QApplication(sys.argv) | ||||||
|  |     recorder: RecorderWindow = RecorderWindow("localhost", 5000) | ||||||
|  |     bot: ExampleBot = ExampleBot() | ||||||
|  |     bot.set_recorder(recorder) | ||||||
|  |  | ||||||
|  |     app.aboutToQuit.connect(recorder.shutdown) | ||||||
|  |     recorder.register_bot(bot) | ||||||
|  |     recorder.show() | ||||||
|  |  | ||||||
|  |     app.exec() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     main() | ||||||
							
								
								
									
										26
									
								
								src/bot.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/bot.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | from __future__ import annotations | ||||||
|  |  | ||||||
|  | from typing import TYPE_CHECKING, Optional | ||||||
|  |  | ||||||
|  | from src.snapshot import Snapshot | ||||||
|  |  | ||||||
|  | if TYPE_CHECKING: | ||||||
|  |     from src.recorder import RecorderWindow | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Bot: | ||||||
|  |     def __init__(self): | ||||||
|  |         self._recorder: Optional[RecorderWindow] = None | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def recorder(self) -> RecorderWindow: | ||||||
|  |         if self._recorder is None: | ||||||
|  |             raise RuntimeError( | ||||||
|  |                 "Bot does not have a recorder. Call Bot.set_recorder to set one") | ||||||
|  |         return self._recorder | ||||||
|  |  | ||||||
|  |     def set_recorder(self, recorder: RecorderWindow): | ||||||
|  |         self._recorder = recorder | ||||||
|  |  | ||||||
|  |     def on_snapshot_received(self, snapshot: Snapshot): | ||||||
|  |         pass | ||||||
							
								
								
									
										22
									
								
								src/car.py
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								src/car.py
									
									
									
									
									
								
							| @@ -8,7 +8,8 @@ from src.remote_controller import RemoteController | |||||||
| from src.utils import get_segments_intersection, segments_intersect | from src.utils import get_segments_intersection, segments_intersect | ||||||
| from src.vec import Vec | from src.vec import Vec | ||||||
|  |  | ||||||
| sign = lambda x: 0 if x == 0 else (-1 if x < 0 else 1) |  | ||||||
|  | def sign(x): return 0 if x == 0 else (-1 if x < 0 else 1) | ||||||
|  |  | ||||||
|  |  | ||||||
| class Car: | class Car: | ||||||
| @@ -27,6 +28,8 @@ class Car: | |||||||
|     RAYS_MAX_DIST = 100 |     RAYS_MAX_DIST = 100 | ||||||
|  |  | ||||||
|     def __init__(self, pos: Vec, direction: Vec) -> None: |     def __init__(self, pos: Vec, direction: Vec) -> None: | ||||||
|  |         self.initial_pos: Vec = pos.copy() | ||||||
|  |         self.initial_dir: Vec = direction.copy() | ||||||
|         self.pos: Vec = pos |         self.pos: Vec = pos | ||||||
|         self.direction: Vec = direction |         self.direction: Vec = direction | ||||||
|         self.speed: float = 0 |         self.speed: float = 0 | ||||||
| @@ -77,7 +80,8 @@ class Car: | |||||||
|         if show_raycasts: |         if show_raycasts: | ||||||
|             pos: Vec = camera.world2screen(self.pos) |             pos: Vec = camera.world2screen(self.pos) | ||||||
|             for p in self.rays_end: |             for p in self.rays_end: | ||||||
|                 pygame.draw.line(surf, (255, 0, 0), pos, camera.world2screen(p), 2) |                 pygame.draw.line(surf, (255, 0, 0), pos, | ||||||
|  |                                  camera.world2screen(p), 2) | ||||||
|  |  | ||||||
|         pts: list[Vec] = self.get_corners() |         pts: list[Vec] = self.get_corners() | ||||||
|         pts = [camera.world2screen(p) for p in pts] |         pts = [camera.world2screen(p) for p in pts] | ||||||
| @@ -127,14 +131,17 @@ class Car: | |||||||
|                             n *= -1 |                             n *= -1 | ||||||
|                             dist = -dist |                             dist = -dist | ||||||
|                         self.speed = 0 |                         self.speed = 0 | ||||||
|                         self.pos = self.pos + n * (self.COLLISION_MARGIN - dist) |                         self.pos = self.pos + n * \ | ||||||
|  |                             (self.COLLISION_MARGIN - dist) | ||||||
|                         return |                         return | ||||||
|  |  | ||||||
|     def cast_rays(self, polygons: list[list[Vec]]): |     def cast_rays(self, polygons: list[list[Vec]]): | ||||||
|         for i in range(self.N_RAYS): |         for i in range(self.N_RAYS): | ||||||
|             angle: float = radians((i / (self.N_RAYS - 1) - 0.5) * self.RAYS_FOV) |             angle: float = radians( | ||||||
|  |                 (i / (self.N_RAYS - 1) - 0.5) * self.RAYS_FOV) | ||||||
|             p: Optional[Vec] = self.cast_ray(angle, polygons) |             p: Optional[Vec] = self.cast_ray(angle, polygons) | ||||||
|             self.rays[i] = self.RAYS_MAX_DIST if p is None else (p - self.pos).mag() |             self.rays[i] = self.RAYS_MAX_DIST if p is None else ( | ||||||
|  |                 p - self.pos).mag() | ||||||
|             self.rays_end[i] = self.pos if p is None else p |             self.rays_end[i] = self.pos if p is None else p | ||||||
|  |  | ||||||
|     def cast_ray(self, angle: float, polygons: list[list[Vec]]) -> Optional[Vec]: |     def cast_ray(self, angle: float, polygons: list[list[Vec]]) -> Optional[Vec]: | ||||||
| @@ -161,3 +168,8 @@ class Car: | |||||||
|                     dist = d |                     dist = d | ||||||
|                     closest = p |                     closest = p | ||||||
|         return closest |         return closest | ||||||
|  |  | ||||||
|  |     def reset(self): | ||||||
|  |         self.pos = self.initial_pos.copy() | ||||||
|  |         self.direction = self.initial_dir.copy() | ||||||
|  |         self.speed = 0 | ||||||
|   | |||||||
| @@ -5,10 +5,14 @@ from enum import IntEnum | |||||||
| import struct | import struct | ||||||
| from typing import Type | from typing import Type | ||||||
|  |  | ||||||
|  | from src.snapshot import Snapshot | ||||||
|  |  | ||||||
|  |  | ||||||
| class CommandType(IntEnum): | class CommandType(IntEnum): | ||||||
|     CAR_CONTROL = 0 |     CAR_CONTROL = 0 | ||||||
|     RECORDING = 1 |     RECORDING = 1 | ||||||
|  |     APPLY_SNAPSHOT = 2 | ||||||
|  |     RESET = 3 | ||||||
|  |  | ||||||
|  |  | ||||||
| class CarControl(IntEnum): | class CarControl(IntEnum): | ||||||
| @@ -30,8 +34,8 @@ class Command(abc.ABC): | |||||||
|             ) |             ) | ||||||
|         Command.REGISTRY[cls.TYPE] = cls |         Command.REGISTRY[cls.TYPE] = cls | ||||||
|  |  | ||||||
|     @abc.abstractmethod |     def get_payload(self) -> bytes: | ||||||
|     def get_payload(self) -> bytes: ... |         return b"" | ||||||
|  |  | ||||||
|     def pack(self) -> bytes: |     def pack(self) -> bytes: | ||||||
|         payload: bytes = self.get_payload() |         payload: bytes = self.get_payload() | ||||||
| @@ -43,8 +47,8 @@ class Command(abc.ABC): | |||||||
|         return Command.REGISTRY[type].from_payload(data[1:]) |         return Command.REGISTRY[type].from_payload(data[1:]) | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     @abc.abstractmethod |     def from_payload(cls, payload: bytes) -> Command: | ||||||
|     def from_payload(cls, payload: bytes) -> Command: ... |         return cls() | ||||||
|  |  | ||||||
|  |  | ||||||
| class ControlCommand(Command): | class ControlCommand(Command): | ||||||
| @@ -82,3 +86,24 @@ class RecordingCommand(Command): | |||||||
|     def from_payload(cls, payload: bytes) -> Command: |     def from_payload(cls, payload: bytes) -> Command: | ||||||
|         state: bool = struct.unpack(">B", payload)[0] |         state: bool = struct.unpack(">B", payload)[0] | ||||||
|         return RecordingCommand(state) |         return RecordingCommand(state) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ApplySnapshotCommand(Command): | ||||||
|  |     TYPE = CommandType.APPLY_SNAPSHOT | ||||||
|  |     __match_args__ = ("snapshot",) | ||||||
|  |  | ||||||
|  |     def __init__(self, snapshot: Snapshot) -> None: | ||||||
|  |         super().__init__() | ||||||
|  |         self.snapshot: Snapshot = snapshot | ||||||
|  |  | ||||||
|  |     def get_payload(self) -> bytes: | ||||||
|  |         return self.snapshot.pack() | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def from_payload(cls, payload: bytes) -> Command: | ||||||
|  |         snapshot: Snapshot = Snapshot.unpack(payload) | ||||||
|  |         return ApplySnapshotCommand(snapshot) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ResetCommand(Command): | ||||||
|  |     TYPE = CommandType.RESET | ||||||
|   | |||||||
| @@ -1,3 +1,4 @@ | |||||||
|  | import lzma | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| import struct | import struct | ||||||
| import time | import time | ||||||
| @@ -12,7 +13,7 @@ class RecordFile: | |||||||
|     def __init__(self, path: str | Path, mode: Literal["w", "r"]) -> None: |     def __init__(self, path: str | Path, mode: Literal["w", "r"]) -> None: | ||||||
|         self.path: str | Path = path |         self.path: str | Path = path | ||||||
|         self.mode: Literal["w", "r"] = mode |         self.mode: Literal["w", "r"] = mode | ||||||
|         self.file = open(self.path, self.mode + "b") |         self.file: lzma.LZMAFile = lzma.LZMAFile(self.path, self.mode) | ||||||
|  |  | ||||||
|     def __enter__(self): |     def __enter__(self): | ||||||
|         return self |         return self | ||||||
| @@ -21,8 +22,7 @@ class RecordFile: | |||||||
|         self.file.close() |         self.file.close() | ||||||
|  |  | ||||||
|     def write_header(self, n_snapshots: int): |     def write_header(self, n_snapshots: int): | ||||||
|         data: bytes = struct.pack( |         data: bytes = struct.pack(">IId", self.VERSION, n_snapshots, time.time()) | ||||||
|             ">IId", self.VERSION, n_snapshots, time.time()) |  | ||||||
|         self.file.write(data) |         self.file.write(data) | ||||||
|  |  | ||||||
|     def write_snapshots(self, snapshots: list[Snapshot]): |     def write_snapshots(self, snapshots: list[Snapshot]): | ||||||
| @@ -35,7 +35,8 @@ class RecordFile: | |||||||
|         version: int = struct.unpack(">I", self.file.read(4))[0] |         version: int = struct.unpack(">I", self.file.read(4))[0] | ||||||
|         if version != self.VERSION: |         if version != self.VERSION: | ||||||
|             raise ValueError( |             raise ValueError( | ||||||
|                 f"Cannot parse record file with format version {version} (current version: {self.VERSION})") |                 f"Cannot parse record file with format version {version} (current version: {self.VERSION})" | ||||||
|  |             ) | ||||||
|  |  | ||||||
|         n_snapshots: int |         n_snapshots: int | ||||||
|         timestamp: float |         timestamp: float | ||||||
|   | |||||||
| @@ -9,7 +9,8 @@ from PyQt6.QtCore import QObject, QThread, QTimer, pyqtSignal, pyqtSlot | |||||||
| from PyQt6.QtGui import QKeyEvent | from PyQt6.QtGui import QKeyEvent | ||||||
| from PyQt6.QtWidgets import QMainWindow | from PyQt6.QtWidgets import QMainWindow | ||||||
|  |  | ||||||
| from src.command import CarControl, Command, ControlCommand, RecordingCommand | from src.bot import Bot | ||||||
|  | from src.command import ApplySnapshotCommand, CarControl, Command, ControlCommand, RecordingCommand, ResetCommand | ||||||
| from src.record_file import RecordFile | from src.record_file import RecordFile | ||||||
| from src.recorder_ui import Ui_Recorder | from src.recorder_ui import Ui_Recorder | ||||||
| from src.snapshot import Snapshot | from src.snapshot import Snapshot | ||||||
| @@ -27,6 +28,7 @@ class RecorderClient(QObject): | |||||||
|             socket.AF_INET, socket.SOCK_STREAM) |             socket.AF_INET, socket.SOCK_STREAM) | ||||||
|         self.timer: Optional[QTimer] = None |         self.timer: Optional[QTimer] = None | ||||||
|         self.connected: bool = False |         self.connected: bool = False | ||||||
|  |         self.buffer: bytes = b"" | ||||||
|  |  | ||||||
|     @pyqtSlot() |     @pyqtSlot() | ||||||
|     def start(self): |     def start(self): | ||||||
| @@ -39,26 +41,26 @@ class RecorderClient(QObject): | |||||||
|         print("Connected to server") |         print("Connected to server") | ||||||
|  |  | ||||||
|     def poll_socket(self): |     def poll_socket(self): | ||||||
|         buffer: bytes = b"" |  | ||||||
|         if not self.connected: |         if not self.connected: | ||||||
|             return |             return | ||||||
|  |  | ||||||
|         try: |         try: | ||||||
|  |             while True: | ||||||
|                 chunk: bytes = self.socket.recv(self.DATA_CHUNK_SIZE) |                 chunk: bytes = self.socket.recv(self.DATA_CHUNK_SIZE) | ||||||
|                 if not chunk: |                 if not chunk: | ||||||
|                     return |                     return | ||||||
|             buffer += chunk |                 self.buffer += chunk | ||||||
|  |  | ||||||
|                 while True: |                 while True: | ||||||
|                 if len(buffer) < 4: |                     if len(self.buffer) < 4: | ||||||
|                         break |                         break | ||||||
|                 msg_len: int = struct.unpack(">I", buffer[:4])[0] |                     msg_len: int = struct.unpack(">I", self.buffer[:4])[0] | ||||||
|                     msg_end: int = 4 + msg_len |                     msg_end: int = 4 + msg_len | ||||||
|                 if len(buffer) < msg_end: |                     if len(self.buffer) < msg_end: | ||||||
|                         break |                         break | ||||||
|  |  | ||||||
|                 message: bytes = buffer[4:msg_end] |                     message: bytes = self.buffer[4:msg_end] | ||||||
|                 buffer = buffer[msg_end:] |                     self.buffer = self.buffer[msg_end:] | ||||||
|                     self.on_message(message) |                     self.on_message(message) | ||||||
|         except BlockingIOError: |         except BlockingIOError: | ||||||
|             pass |             pass | ||||||
| @@ -162,9 +164,11 @@ class RecorderWindow(Ui_Recorder, QMainWindow): | |||||||
|         self.recordDataButton.clicked.connect(self.toggle_record) |         self.recordDataButton.clicked.connect(self.toggle_record) | ||||||
|         self.resetButton.clicked.connect(self.rollback) |         self.resetButton.clicked.connect(self.rollback) | ||||||
|  |  | ||||||
|  |         self.bot: Optional[Bot] = None | ||||||
|         self.autopiloting = False |         self.autopiloting = False | ||||||
|  |  | ||||||
|         self.autopilotButton.clicked.connect(self.toggle_autopilot) |         self.autopilotButton.clicked.connect(self.toggle_autopilot) | ||||||
|  |         self.autopilotButton.setDisabled(True) | ||||||
|  |  | ||||||
|         self.saveRecordButton.clicked.connect(self.save_record) |         self.saveRecordButton.clicked.connect(self.save_record) | ||||||
|  |  | ||||||
| @@ -203,7 +207,19 @@ class RecorderWindow(Ui_Recorder, QMainWindow): | |||||||
|         self.send_command(RecordingCommand(self.recording)) |         self.send_command(RecordingCommand(self.recording)) | ||||||
|  |  | ||||||
|     def rollback(self): |     def rollback(self): | ||||||
|         pass |         rollback_by: int = self.forgetSnapshotNumber.value() | ||||||
|  |         rollback_by = max(0, min(rollback_by, len(self.snapshots) - 1)) | ||||||
|  |  | ||||||
|  |         self.snapshots = self.snapshots[:-rollback_by] | ||||||
|  |         self.nbrSnapshotSaved.setText(str(len(self.snapshots))) | ||||||
|  |  | ||||||
|  |         if len(self.snapshots) == 0: | ||||||
|  |             self.send_command(ResetCommand()) | ||||||
|  |         else: | ||||||
|  |             self.send_command(ApplySnapshotCommand(self.snapshots[-1])) | ||||||
|  |  | ||||||
|  |         if self.recording: | ||||||
|  |             self.toggle_record() | ||||||
|  |  | ||||||
|     def toggle_autopilot(self): |     def toggle_autopilot(self): | ||||||
|         self.autopiloting = not self.autopiloting |         self.autopiloting = not self.autopiloting | ||||||
| @@ -227,7 +243,7 @@ class RecorderWindow(Ui_Recorder, QMainWindow): | |||||||
|  |  | ||||||
|         self.SAVE_DIR.mkdir(exist_ok=True) |         self.SAVE_DIR.mkdir(exist_ok=True) | ||||||
|  |  | ||||||
|         record_name: str = "record_%d.rec" |         record_name: str = "record_%d.rec.xz" | ||||||
|         fid = 0 |         fid = 0 | ||||||
|         while os.path.exists(self.SAVE_DIR / (record_name % fid)): |         while os.path.exists(self.SAVE_DIR / (record_name % fid)): | ||||||
|             fid += 1 |             fid += 1 | ||||||
| @@ -251,8 +267,15 @@ class RecorderWindow(Ui_Recorder, QMainWindow): | |||||||
|         self.snapshots.append(snapshot) |         self.snapshots.append(snapshot) | ||||||
|         self.nbrSnapshotSaved.setText(str(len(self.snapshots))) |         self.nbrSnapshotSaved.setText(str(len(self.snapshots))) | ||||||
|  |  | ||||||
|  |         if self.autopiloting and self.bot is not None: | ||||||
|  |             self.bot.on_snapshot_received(snapshot) | ||||||
|  |  | ||||||
|     def shutdown(self): |     def shutdown(self): | ||||||
|         self.close_signal.emit() |         self.close_signal.emit() | ||||||
|  |  | ||||||
|     def send_command(self, command: Command): |     def send_command(self, command: Command): | ||||||
|         self.send_signal.emit(command) |         self.send_signal.emit(command) | ||||||
|  |  | ||||||
|  |     def register_bot(self, bot: Bot): | ||||||
|  |         self.bot = bot | ||||||
|  |         self.autopilotButton.setDisabled(False) | ||||||
|   | |||||||
| @@ -99,7 +99,7 @@ | |||||||
|       <item> |       <item> | ||||||
|        <widget class="QPushButton" name="resetButton"> |        <widget class="QPushButton" name="resetButton"> | ||||||
|         <property name="text"> |         <property name="text"> | ||||||
|          <string>Reset</string> |          <string>Rollback</string> | ||||||
|         </property> |         </property> | ||||||
|        </widget> |        </widget> | ||||||
|       </item> |       </item> | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| # Form implementation generated from reading ui file 'recorder.ui' | # Form implementation generated from reading ui file 'recorder.ui' | ||||||
| # | # | ||||||
| # Created by: PyQt6 UI code generator 6.8.0 | # Created by: PyQt6 UI code generator 6.8.1 | ||||||
| # | # | ||||||
| # WARNING: Any manual changes made to this file will be lost when pyuic6 is | # WARNING: Any manual changes made to this file will be lost when pyuic6 is | ||||||
| # run again.  Do not edit this file unless you know what you are doing. | # run again.  Do not edit this file unless you know what you are doing. | ||||||
| @@ -102,7 +102,7 @@ class Ui_Recorder(object): | |||||||
|         self.recordDataButton.setText(_translate("Recorder", "Record")) |         self.recordDataButton.setText(_translate("Recorder", "Record")) | ||||||
|         self.saveImgCheckBox.setText(_translate("Recorder", "Imgs")) |         self.saveImgCheckBox.setText(_translate("Recorder", "Imgs")) | ||||||
|         self.saveRecordButton.setText(_translate("Recorder", "Save")) |         self.saveRecordButton.setText(_translate("Recorder", "Save")) | ||||||
|         self.resetButton.setText(_translate("Recorder", "Reset")) |         self.resetButton.setText(_translate("Recorder", "Rollback")) | ||||||
|         self.nbrSnapshotSaved.setText(_translate("Recorder", "0")) |         self.nbrSnapshotSaved.setText(_translate("Recorder", "0")) | ||||||
|         self.autopilotButton.setText(_translate("Recorder", "AutoPilot\n" |         self.autopilotButton.setText(_translate("Recorder", "AutoPilot\n" | ||||||
| "OFF")) | "OFF")) | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ import struct | |||||||
| import threading | import threading | ||||||
| from typing import TYPE_CHECKING, Optional | from typing import TYPE_CHECKING, Optional | ||||||
|  |  | ||||||
| from src.command import CarControl, Command, ControlCommand, RecordingCommand | from src.command import ApplySnapshotCommand, CarControl, Command, ControlCommand, RecordingCommand, ResetCommand | ||||||
| from src.snapshot import Snapshot | from src.snapshot import Snapshot | ||||||
| from src.utils import RepeatTimer | from src.utils import RepeatTimer | ||||||
|  |  | ||||||
| @@ -119,6 +119,10 @@ class RemoteController: | |||||||
|                 self.set_control(control, active) |                 self.set_control(control, active) | ||||||
|             case RecordingCommand(state): |             case RecordingCommand(state): | ||||||
|                 self.recording = state |                 self.recording = state | ||||||
|  |             case ApplySnapshotCommand(snapshot): | ||||||
|  |                 snapshot.apply(self.car) | ||||||
|  |             case ResetCommand(): | ||||||
|  |                 self.car.reset() | ||||||
|  |  | ||||||
|     def set_control(self, control: CarControl, active: bool): |     def set_control(self, control: CarControl, active: bool): | ||||||
|         setattr(self.car, self.CONTROL_ATTRIBUTES[control], active) |         setattr(self.car, self.CONTROL_ATTRIBUTES[control], active) | ||||||
|   | |||||||
| @@ -63,7 +63,7 @@ class Snapshot: | |||||||
|         (nbr_raycasts,), data = iter_unpack(">B", data) |         (nbr_raycasts,), data = iter_unpack(">B", data) | ||||||
|         raycast_distances, data = iter_unpack(f">{nbr_raycasts}f", data) |         raycast_distances, data = iter_unpack(f">{nbr_raycasts}f", data) | ||||||
|  |  | ||||||
|         (h, w), data = iter_unpack(">ii", data) |         (h, w), data = iter_unpack(">II", data) | ||||||
|  |  | ||||||
|         if h * w > 0: |         if h * w > 0: | ||||||
|             image = np.frombuffer(data, np.uint8).reshape(h, w, 3) |             image = np.frombuffer(data, np.uint8).reshape(h, w, 3) | ||||||
| @@ -94,3 +94,8 @@ class Snapshot: | |||||||
|             raycast_distances=car.rays.copy(), |             raycast_distances=car.rays.copy(), | ||||||
|             image=None |             image=None | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |     def apply(self, car: Car): | ||||||
|  |         car.pos = self.position.copy() | ||||||
|  |         car.direction = self.direction.copy() | ||||||
|  |         car.speed = 0 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user