The rover emulator simulates multiple autonomous rovers by reading telemetry data from predefined files and streaming it over UDP on localhost. The emulator reads position and LiDAR data at a fixed 10Hz frequency.
For debugging purposes, a commandline argument can be specified to elminate simulated sensor noise in the readings. The solution will be evaluated off of its ability to handle the program running without the --no-noise flag, however. See Running the Emulator for details
/project_root
│── emulator/
│ ├── rover_emulator.cpp # Main source file
│ ├── rover_profiles.h # Rover profile definitions
│── data/ # Contains rover data files
│── run_rovers.sh # Script to launch multiple rovers
│── Makefile # Compilation instructions
To compile the emulator, run:
makeTo clean up compiled files:
make cleanData files required for the emulator are .gz compressed inside data/
run make extract to extract them.
To run the full set of 5-rover emulators with full noise characteristics, run
make runTo run without noise for debugging purposes, run
make run-noiselessTo manually start an emulator for a specific rover with an optional --no-noise flag
./rover_emulator <ROVER_ID>Example:
./rover_emulator 1To run five concurrent rover instances, use:
./run_rovers.shTo start the LiDAR rover visualization, run:
cmake -B ./build -DCMAKE_BUILD_TYPE=Release
cmake --build ./build --parallel
./build/lidar_viewer
For best results, run noiseless.
If run_rovers.sh is terminated, all running rover instances are killed automatically.
Each rover sends:
- Pose (Position/Orientation) Data on port
9000 + RoverID. - LiDAR (Point Cloud) Data on port
10000 + RoverID.
All traffic is UDP on localhost (127.0.0.1).
Example:
- Rover ID=1 → Pose on port
9001, LiDAR on port10001. - Rover ID=2 → Pose on port
9002, LiDAR on port10002. - etc.
More details can be found in
emulator/rover_profiles.h
Each pose packet is a binary structure sent in a single UDP datagram:
#pragma pack(push, 1)
struct PosePacket {
double timestamp; // Seconds since emulator start
float posX; // X position (units)
float posY; // Y position (units)
float posZ; // Z position (units)
float rotXdeg; // Roll or X rotation (degrees)
float rotYdeg; // Pitch or Y rotation (degrees)
float rotZdeg; // Yaw or Z rotation (degrees)
};
#pragma pack(pop)- Timestamp is a
doublerepresenting seconds since the emulator started.
LiDAR data is chunked so it can fit within safe UDP packet sizes. Each LiDAR “scan” from the data file is broken into N chunks. Each chunk is a binary structure:
static const size_t MAX_LIDAR_POINTS_PER_PACKET = 100;
#pragma pack(push, 1)
struct LidarPacketHeader {
double timestamp; // Same as PosePacket timestamp
uint32_t chunkIndex; // Which chunk this is
uint32_t totalChunks; // Total chunks for this scan
uint32_t pointsInThisChunk;
};
struct LidarPoint {
float x;
float y;
float z;
};
struct LidarPacket {
LidarPacketHeader header;
LidarPoint points[MAX_LIDAR_POINTS_PER_PACKET];
};
#pragma pack(pop)- Each chunk is sent in one UDP datagram.
- The actual bytes sent (
packetSize) =sizeof(LidarPacketHeader) + (pointsInThisChunk * sizeof(LidarPoint)). chunkIndexruns from0tototalChunks - 1.pointsInThisChunkis how many points are in the current chunk.timestampin the header matches the PosePacket timestamp for correlation.
Example Calculation:
- If a LiDAR scan has 350 points, it’s split into 4 chunks (3 full chunks of 100 points, and 1 chunk of 50 points).
- The receiver can reassemble the full point cloud for that timestamp by collecting chunks 0..3.
Each rover listens for button state commands and transmits the current button states.
- Port:
8000 + RoverID(e.g.,8001for rover1) - Format: 1-byte (
uint8_t) UDP message, where each bit represents a button state:- Bit 0 → Button 0 (1 = ON, 0 = OFF)
- Bit 1 → Button 1 (1 = ON, 0 = OFF)
- Bit 2 → Button 2 (1 = ON, 0 = OFF)
- Bit 3 → Button 3 (1 = ON, 0 = OFF)
- The rover updates internal button states upon receiving a command.
-
Port:
11000 + RoverID(e.g.,11001for rover1) -
Sent at: 10Hz (same as pose & LiDAR)
-
Format:
VehicleTelemstruct:#pragma pack(push, 1) struct VehicleTelem { double timestamp; // Same as PosePacket timestamp uint8_t buttonStates; // Bitfield for button states (4 bits used) }; #pragma pack(pop)
-
Example:
- A
buttonStatesvalue of0b00001001(9in decimal) means:- Button 0: ON
- Button 1: OFF
- Button 2: OFF
- Button 3: ON
- A
- Four Ports per Rover:
- Pose Data (
9000 + RoverID) - LiDAR Data (
10000 + RoverID) - Button Telemetry (
11000 + RoverID) - Button Commands (
8000 + RoverID)
- Pose Data (
- Pose: Single struct with timestamp, position (XYZ), and rotation (XYZ in degrees).
- LiDAR: Multiple chunks per scan, each chunk includes a header plus an array of
(x,y,z)points. - Buttons: Receives 1-byte bitfield commands and reports 1-byte bitfield telemetry.
- All traffic is sent via UDP to localhost at a 10 Hz rate.