Sensor Stream#
The sensor stream is a binary socket for the sensor data of one agent: clock, INS data, LiDAR clouds and camera images. This page gives the protocol as the game writes it and as the ROS 2 bridge reads it.
The game opens the stream with -SensorStream=<port>. The game has no default port.
The ROS 2 launch files, the Core server and the tools use port 5601.
The ROS 2 bridge changes the frames into ROS 2 messages: refer to Topics.
The other two sockets of a session are the Vehicle Bridge and the Simulator Control Channel.
| Item | Type | Function |
|---|---|---|
-SensorStream= |
Option | Opens the stream of agent 0. |
-SensorStreams= |
Option | Opens one stream for each agent. |
| Transport | Protocol | TCP on 127.0.0.1, one client, binary frames. |
| Frame Header | Protocol | The 32 bytes at the start of each frame. |
| Units and Frames | Protocol | The time bases, the coordinate frames and the units. |
| Hello (1) | Frame | The sensor layout of the agent as JSON. |
| Clock (2) | Frame | The time of each physics step. |
| INS (3) | Frame | One INS epoch: 40 values. |
| LiDAR (4) | Frame | One organised cloud. |
| Camera (5) | Frame | One image. |
| Barrier (6) | Frame | The end of a lockstep step. |
| Rates and Sizes | Protocol | The frame rates and the data volume. |
| Flow Control and Drops | Protocol | The queue limits and the drop counters. |
| Log Lines | Protocol | The lines that the game writes to its log. |
| Python Reader | Example | A reader for the header and the LiDAR frame. |
| Differences in ACRES Core | Table | The behaviour of the Core server. |
Options#
-SensorStream=#
Opens the sensor stream of agent 0 on 127.0.0.1 at the given port.
The option also starts the sensor recorder of the agent. Without -SensorRecord, the recorder writes no files.
A vehicle that has no INS in its sensor configuration gets one. The Maxxum is such a vehicle.
The INS then reports the point base_footprint.
| Name | Type | Unit | Default | Description |
|---|---|---|---|---|
-SensorStream= |
integer | no stream | The TCP port, from 1 to 65535. | |
sensor_stream |
integer | no stream | The same value as a key of one agent in the file of -Agents=. The key has priority. |
-SensorStreams=#
Opens one sensor stream for each agent of a session with more than one vehicle.
The value is a list of ports with commas. The port at index N is the port of agent N.
The game uses the list for an agent only when the agent has no port from -SensorStream= or from its key sensor_stream.
Each stream has its own sender thread, its own queue and its own sequence numbers. The stamps of all streams use one clock, the simulation time of the world.
| Name | Type | Unit | Default | Description |
|---|---|---|---|---|
-SensorStreams= |
list of integers | no stream | One TCP port for each agent, in the sequence of the agents. |
ACRES_SENSOR_STREAM_LISTENING agent=polaris port=5601
ACRES_SENSOR_STREAM_LISTENING agent=maxxum port=5602
The Python Reader on the port of the Maxxum:
Protocol#
Transport#
The stream is a TCP socket. The game listens on 127.0.0.1 only. The data does not go out of the computer.
- One client reads the stream at a time. A second client stays in the listen queue until the first client disconnects.
- The game accepts a client only when the sensor recorder knows its layout.
- The first frame on each connection is the Hello frame.
- The game does not read from the socket. The client sends no data.
- The game queues frames only while it has a client. Data from the time before the connection is not in the stream.
- The socket has
TCP_NODELAYand a send buffer request of 16 MiB.
All numbers are little-endian. A frame is a header of 32 bytes and a payload.
Frame Header#
Each frame starts with this header. PutHeader in AcresSensorStream.cpp writes it and parse_frame_header in stream.hpp reads it.
| Offset | Type | Field | Contents |
|---|---|---|---|
| 0 | char[4] |
magic | The characters TSS1. |
| 4 | uint16 |
type | The frame type, 1 to 6. |
| 6 | uint16 |
flags | Always 0. |
| 8 | uint32 |
sequence | A counter for all frame types of this stream. |
| 12 | uint32 |
payload bytes | The number of bytes after the header. |
| 16 | float64 |
stamp | The simulation time of the data in seconds. |
| 24 | float64 |
publish time | The time of CLOCK_MONOTONIC in seconds when the game queued the frame. |
The sequence increases by 1 for each frame that the game queues. A dropped frame gets no number. The counter does not start again at a new connection.
A client that reads CLOCK_MONOTONIC on the same computer can measure the transport time: its clock minus the publish time.
The Python function time.monotonic() reads that clock on Linux.
The ROS 2 bridge closes the connection and connects again when the magic is not TSS1.
The first frames of a session with the Polaris:
Units and Frames#
| Quantity | Convention |
|---|---|
| Stamp | The physics clock of the world in seconds. All agents of a session use this clock. |
| Vehicle time | The physics time of one agent. The stamp is the vehicle time plus a constant offset. |
| Publish time | CLOCK_MONOTONIC of the computer in seconds. |
| Body frame | FLU: x forward, y to the left, z up, in metres. |
base_footprint |
The middle of the rear axle on the ground. The INS reports this point. |
| World frame | ENU on the map grid of the tile: x east, y north, z up, in metres. |
| UTM | Easting and northing in metres in the zone of the Hello frame (16 for ACRE). |
| Quaternion | The sequence x, y, z, w. It rotates a vector from the body frame into the reference frame. |
| LiDAR points | The sensor frame of the LiDAR (FLU) in metres. |
| Image | Rows from the top, 8 bits for each channel, channel sequence blue, green, red. |
The stream has no modelled delay. The keys delay_s and lidar.delay_s apply only to the files of the session log.
A LiDAR frame and a camera frame go out when the game completes them. Their stamps give the time of the measurement.
Frame Types#
Hello (1)#
The first frame of each connection. The payload is one JSON object in UTF-8. The stamp is 0.
FAcresSensorRecorder::StreamHello makes the text.
| Name | Type | Unit | Default | Description |
|---|---|---|---|---|
schema |
string | acres-sensor-stream-1. |
||
agent |
string | The name of the agent. | ||
vehicle |
string | maxxum or polaris. |
||
georeferenced |
boolean | true when the map has a geographic reference. |
||
mount_convention |
string | A text that describes the mount poses. | ||
body_origin_flu_m |
array of 3 | m | The origin of the vehicle body, seen from base_footprint. |
|
lidar.enabled |
boolean | true when the agent has a LiDAR. |
||
lidar.frame_id |
string | The frame name of the cloud. | ||
lidar.hz |
number | Hz | The scan rate. | |
lidar.columns, lidar.rings |
integer | The size of the organised cloud. | ||
lidar.range_m |
number | m | The maximum range. | |
lidar.azimuth_order |
string | cw or ccw: the direction of the column index. |
||
lidar.position_flu_m, lidar.rpy_deg |
array of 3 | m, deg | The mount pose from base_footprint. |
|
camera.enabled |
boolean | true when the camera renders. |
||
camera.hz |
number | Hz | The image rate. | |
camera.width, camera.height |
integer | px | The size of the image. | |
camera.fx, fy, cx, cy |
number | px | The intrinsics of the image (OpenCV convention). | |
camera.distortion_model |
string | plumb_bob. |
||
camera.d |
array of 5 | \(k_1, k_2, p_1, p_2, k_3\). All 0 when the lens model is off. | ||
camera.encoding |
string | bgr8. |
||
camera.position_flu_m, camera.rpy_deg |
array of 3 | m, deg | The mount pose from base_footprint. |
|
ins.enabled |
boolean | true when the agent sends INS frames. |
||
ins.hz |
number | Hz | The INS rate. | |
ins.position_flu_m |
array of 3 | m | The INS point from base_footprint. |
|
ins.utm_zone |
integer | The UTM zone. | ||
ins.datum |
string | nad83_2011 or wgs84_g2139. |
||
ins.imu_position_flu_m, ins.gnss_antenna_flu_m |
array of 3 | m | The mount positions of the IMU and the antenna. |
The rotations are roll, pitch and yaw about the fixed FLU axes, \(R = R_z R_y R_x\). The Hello frame gives the calibrated mounts. A hardware shift from the simulator control channel does not change it.
The Hello frame of the Polaris, with shortened numbers:
{
"schema": "acres-sensor-stream-1",
"agent": "polaris",
"vehicle": "polaris",
"georeferenced": true,
"body_origin_flu_m": [1.14808, 0, 0.65536],
"lidar": {
"enabled": true, "frame_id": "lidar", "hz": 10,
"columns": 1800, "rings": 32, "range_m": 161.3,
"azimuth_order": "cw",
"position_flu_m": [2.5, 0, 2.039],
"rpy_deg": [-1.518, 5.317, 0]
},
"camera": {
"enabled": true, "hz": 10,
"width": 896, "height": 512,
"fx": 599.526, "fy": 592.378,
"cx": 440.130, "cy": 270.805,
"distortion_model": "plumb_bob",
"d": [-0.55389, 0.40842, -0.0099785,
0.0044239, -0.16673],
"encoding": "bgr8",
"position_flu_m": [2.745, 0.606, 1.887],
"rpy_deg": [-3.49, 16.52, -2.63]
},
"ins": {
"enabled": true, "hz": 100,
"position_flu_m": [0, 0, 0],
"utm_zone": 16, "datum": "nad83_2011",
"imu_position_flu_m": [0.95, -0.45, 0.7],
"gnss_antenna_flu_m": [1.1, 0, 2.15]
}
}
Clock (2)#
One frame for each physics step: 120 frames in each second of simulation time. The physics thread queues it.
| Offset | Type | Field | Contents |
|---|---|---|---|
| 0 | float64 |
vehicle time | The physics time of the agent in seconds. |
The stamp of the header is the same time on the clock of the world.
The ROS 2 bridge of agent 0 publishes the stamp on the topic /clock.
INS (3)#
One INS epoch at the rate ins.hz. The payload is 40 values of the type float64: 320 bytes.
The values with an index from 0 to 31 contain the errors of the INS model. The values from 32 to 39 are the exact values of the simulation.
INS and GNSS gives the model.
| Index | Field | Unit | Contents |
|---|---|---|---|
| 0, 1 | UtmE, UtmN |
m | UTM easting and northing. |
| 2 | UtmH |
m | Ellipsoidal height of the fix. |
| 3 to 6 | UtmQx, UtmQy, UtmQz, UtmQw |
Attitude in the UTM grid: x grid east, y grid north, z up. | |
| 7 to 9 | VelX, VelY, VelZ |
m/s | Velocity in the body frame. |
| 10 to 12 | GyroX, GyroY, GyroZ |
rad/s | Angular rate in the body frame. |
| 13 to 15 | AccelX, AccelY, AccelZ |
m/s² | Specific force in the body frame. At rest, z is +9.81. |
| 16, 17 | Lat, Lon |
deg | Latitude and longitude in the datum of the INS. |
| 18 | Height |
m | Ellipsoidal height. |
| 19 to 21 | CovE, CovN, CovU |
m² | Diagonal of the position covariance: east, north, up. |
| 22 to 25 | EnuQx, EnuQy, EnuQz, EnuQw |
Attitude, body frame to world frame. | |
| 26 to 28 | EnuX, EnuY, EnuZ |
m | Position in the world frame. |
| 29 | FixStatus |
The status of sensor_msgs/NavSatStatus: 2 for an RTK fix, -1 without a geographic reference. |
|
| 30 | GridRotationDeg |
deg | UTM grid azimuth of the grid north of the map. |
| 31 | UtmZone |
The UTM zone. | |
| 32 to 34 | TruthEnuX, TruthEnuY, TruthEnuZ |
m | Exact position of base_footprint in the world frame. |
| 35 to 38 | TruthQx, TruthQy, TruthQz, TruthQw |
Exact attitude, body frame to world frame. | |
| 39 | TruthSpeed |
m/s | Exact forward speed. |
Without a geographic reference, the values 0 to 6, 16 to 21, 30 and 31 are NaN.
The INS rate does not have to divide 120 Hz. An epoch is the first physics step at or after its due time. At 100 Hz the interval between two stamps is thus 1/120 s or 2/120 s, with a mean of 0.01 s.
LiDAR (4)#
One organised cloud for each scan. A thread-pool task queues it when the scan is complete. The stamp is the end of the sweep.
| Offset | Type | Field | Contents |
|---|---|---|---|
| 0 | uint32 |
columns | The number of azimuth steps. This is the height of the cloud. |
| 4 | uint32 |
rings | The number of rings. This is the width of the cloud. |
| 8 | uint32 |
point step | 16: the number of bytes of one point. |
| 12 | uint32 |
reserved | 0. |
| 16 | float32[4] x columns x rings |
points | x, y, z, intensity for each cell. |
- The cell of column \(c\) and ring \(r\) has the index \(c \cdot \text{rings} + r\).
x,yandzare in the sensor frame of the LiDAR in metres.intensityis the reflectivity byte, 0 to 255, as afloat32.- A beam without a return has NaN in
x,yandzand the intensity 0. - Each beam has one return: the strongest return of the beam.
The points are in the sensor frame at the firing time of their column. The frame has no time for each point. LiDAR gives the column sequence, the ring table and the firing times.
Camera (5)#
One image for each camera sample. A thread-pool task queues it after the camera model. The stamp is the simulation time of the sample.
| Offset | Type | Field | Contents |
|---|---|---|---|
| 0 | uint32 |
width | The width of the image in pixels. |
| 4 | uint32 |
height | The height of the image in pixels. |
| 8 | uint32 |
encoding | 1: bgr8. |
| 12 | uint32 |
row step | The number of bytes of one row: 3 x width. |
| 16 | uint8[] |
pixels | height x row step bytes. The top row is first. |
The image is the output of the camera model: lens distortion, rolling shutter and sensor noise. The Hello frame gives the intrinsics. Camera gives the model.
Barrier (6)#
The marker at the end of a lockstep step. The game sends it only in lockstep mode.
When a step request of the Simulator Control Channel is complete, the game queues one barrier on each sensor stream.
| Offset | Type | Field | Contents |
|---|---|---|---|
| 0 | uint64 |
step | The physics step of the world that the request reached. |
All clock, INS, LiDAR and camera frames of the requested steps are in the stream before the barrier. The game waits until the sensor recorder has no scan and no image in work, and then sends the barrier. The stamp of the header is the latest physics time of the agent.
Lockstep Stepping gives the procedure.
Three step requests of 12 steps each, in a session with the Polaris:
step=12 frames: clock 12, ins 10, lidar 1, camera 1
barrier sequence=25 stamp=0.100000 payload=8 step=12
step=24 frames: clock 12, ins 10, lidar 1, camera 1
barrier sequence=50 stamp=0.200000 payload=8 step=24
step=36 frames: clock 12, ins 10, lidar 1, camera 1
barrier sequence=75 stamp=0.300000 payload=8 step=36
Before the first request, the stream contains only the Hello frame.
Behaviour#
Rates and Sizes#
| Frame | Rate | Payload |
|---|---|---|
| Hello | One time for each connection | Approximately 1.2 kB |
| Clock | 120 Hz | 8 bytes |
| INS | ins.hz, 100 Hz |
320 bytes |
| LiDAR | lidar.hz, 10 Hz |
16 + 16 x columns x rings bytes |
| Camera | camera.hz, 10 Hz |
16 + 3 x width x height bytes |
| Barrier | One for each step request |
8 bytes |
The Polaris sends 921 616 bytes for each cloud and 1 376 272 bytes for each image: approximately 23 MB in each second.
The frames are not in the sequence of their stamps. The physics thread queues the clock frame and the INS frame in their step. A cloud and an image come later, when the game completes them.
A run of 30 s with the Polaris on an RTX 5060 Ti. The transport time is the receive time minus the publish time.
Flow Control and Drops#
Each stream has one sender thread and one queue. A slow client does not stop the physics, the game thread or the renderer.
| Condition | Result |
|---|---|
| No client | The game queues no frames. |
| Queue plus the new frame above 96 MiB | The game drops a LiDAR frame or a camera frame. |
| Queue plus the new frame above 192 MiB | The game drops all frame types. |
| The send operation fails | The game closes the connection and clears the queue. |
| The client closes the connection | The game clears the queue and waits for the next client. |
| A new client connects | The game sends a new Hello frame. The sequence and the counters of the log continue. |
A dropped frame gets no sequence number, thus a client cannot find a drop from the sequence. The game counts the drops for each frame type and writes them to its log.
Log Lines#
| Line | Time |
|---|---|
ACRES_SENSOR_STREAM_LISTENING agent= port= |
The socket is open. |
ACRES_SENSOR_STREAM_LISTEN_FAILED agent= port= |
The game cannot open the port. The session continues without the stream. |
ACRES_SENSOR_STREAM_CONNECTED agent= port= send_buffer= |
A client connected and the Hello frame went out. |
ACRES_SENSOR_STREAM_STATS agent= ... |
Each 30 s while the stream has a client. |
ACRES_SENSOR_STREAM_DISCONNECTED agent= port= reason= ... |
The connection closed. The reason is peer closed, send failed or hello failed. |
ACRES_SENSOR_STREAM_STOP agent= ... |
The session ended. |
The statistics text does not show the barrier frames.
Python Reader#
Python Reader#
The example connects to the stream, reads the header of each frame and decodes the first LiDAR cloud. It needs Python 3 and NumPy.
-
Start the game with a sensor stream.
-
Run the example when the log shows
ACRES_SENSOR_STREAM_LISTENING.
Expected Result
The example prints the agent of the Hello frame and the first cloud. The number of returns changes with the place of the vehicle.
For ROS 2 clients, use the ROS 2 bridge. The C++ functions of ROS/acres_sim/include/acres_sim/stream.hpp read the same frames without ROS 2.
import json
import socket
import struct
import numpy as np
# magic, type, flags, sequence, payload bytes, stamp, publish time
HEADER = struct.Struct("<4sHHIIdd")
def read_exact(sock, count):
data = bytearray()
while len(data) < count:
chunk = sock.recv(count - len(data))
if not chunk:
raise ConnectionError("the stream closed")
data += chunk
return bytes(data)
def read_frame(sock):
(magic, kind, flags, sequence, size,
stamp_s, publish_s) = HEADER.unpack(read_exact(sock, 32))
if magic != b"TSS1":
raise ValueError("bad magic")
return kind, sequence, stamp_s, read_exact(sock, size)
def lidar_cloud(payload):
columns, rings, _, _ = struct.unpack_from("<4I", payload)
points = np.frombuffer(payload, "<f4", columns * rings * 4, 16)
return points.reshape(columns, rings, 4)
sock = socket.create_connection(("127.0.0.1", 5601))
while True:
kind, sequence, stamp_s, payload = read_frame(sock)
if kind == 1:
hello = json.loads(payload)
print("hello", hello["agent"], hello["lidar"]["columns"],
"x", hello["lidar"]["rings"])
elif kind == 4:
cloud = lidar_cloud(payload)
returns = np.isfinite(cloud[:, :, 0]).sum()
print(f"lidar seq={sequence} stamp={stamp_s:.3f} s "
f"shape={cloud.shape} returns={returns}")
break
Differences in ACRES Core#
The Core server core_sim of the package acres_core_sim serves the same protocol. The ROS 2 bridge reads it without a change.
| Item | Game | Core Server |
|---|---|---|
| Option | -SensorStream=<port>, no default. |
--sensor-ports <port>, one port for each agent. The launch file uses 5601. |
| Hello frame | Sequence from the counter, stamp 0. | Sequence 0, stamp at the world time. camera.enabled is false. |
| Camera frames | Yes. | No. ACRES Core has no camera. |
| LiDAR frames | The GPU path or the CPU path of the game. | The ray-cast LiDAR of ACRES Core. |
| Barrier | After the sensors are idle. | After the LiDAR scans of the steps. |