Developer Documentation
This page covers building Playarea from source and the network protocol the simulator speaks. For installing the prebuilt package and a quick tour, see Getting Started.
Build
The simulator is a C++ app built with xmake:
xmake build
Dependencies are resolved automatically: threepp comes from Conan Center
(conan::threepp in xmake.lua); the rest come from xmake's own package repo
(xrepo). On Linux you also need the system X11/GL/audio dev headers (GLFW/OpenGL
and OpenAL build from source there).
Network Interface
The robot splits control and feedback across two transports so each uses the right tool:
- Control over TCP (default port
9000,--tcp_port) — reliable delivery, because a dropped command is bad. The protocol is plain ASCII, so you can drive it by hand withnc localhost 9000. - Feedback over UDP — compact binary sensor frames, because for a fast telemetry loop a fresh-but-lost frame beats one stuck behind TCP retransmits (head-of-line blocking).
Feedback is split into three independent channels a client subscribes to one at a time, each streamed to a UDP port of the client's choosing (so every channel can live on its own socket):
| Channel | Frame magic | Contents |
|---|---|---|
odom | ODM1 | Wheel-odometry pose estimate, at --tcp_feedback_hz (100 Hz). |
scan | SCN1 | Fake 360° planar laser scan, at --scan_hz (10 Hz). |
camera | CAM1 | Head-camera POV image (RGB888, chunked), at --camera_hz (15 Hz). |
Commands (client → server, TCP):
| Command | Effect |
|---|---|
cmd <v> <w> [head] | Set linear (m/s), angular (rad/s) and optional head (rad/s). |
stop | Zero every command. |
sub <channel> <udp_port> | Stream a channel (odom|scan|camera) to this client's address on the given UDP port. |
unsub <channel> | Stop that channel's stream. |
UDP frames (server → client, all little-endian):
odom magic "ODM1" | uint32 seq | float64 t[s] | 5× float32: x y theta v w
(struct <4sId5f>, 36 bytes)
scan magic "SCN1" | uint32 seq | float64 t[s] |
float32 angle_min, angle_increment, range_min, range_max | uint32 count |
count× float32 ranges[m] (struct <4sId4fI> + <Nf>)
camera magic "CAM1" | uint16 w,h | uint32 size,offset |
uint16 chunk_index, chunk_count | RGB888 payload (struct <4sIHHIIHH> + bytes)
— one image split across chunks; reassemble by seq using offset.
Each frame's sequence number lets a client detect dropped or reordered frames; the timestamp is seconds since server start. Every subscription is tied to its TCP connection and stops automatically when that connection closes.
All values are SI: x/y in metres, theta heading in radians (about the z
axis, wrapped to [-π, π]), v in m/s, w in rad/s. Scan ranges are metres in
the sensor frame (a beam at range_max means "no return"). Kinematics and
odometry are integrated on a dedicated fixed-rate thread (--control_hz, default
200 Hz) decoupled from the render frame rate, so the control/feedback loop stays
fast and jitter-free. The laser scanner raycasts the same obstacle footprints the
robot collides with, and the head camera is an extra offscreen render of the
robot's point of view — captured only while a client is subscribed.
Python client
A Python client wrapping this protocol ships in the
playarea package —
from playarea import Robot
(client.py).
The uv-based demos in
examples/00-simple/
use it to drive the robot in a circle and print odometry, plot the laser scanner
RViz-style, and view the head-camera stream live:
cd examples/00-simple && uv run main.py # or: uv run scan.py / uv run camera.py
Map format & coordinate conventions
The simulator loads a ROS-style occupancy grid (dark pixels are walls, white is
free space) plus a map.yaml metadata file (resolution, origin, thresholds).
The default world (11.09 × 11.09 m at 0.022178 m/px, 500 × 500 px) matches
the sim's built-in map. Point the simulator at a YAML pair:
playarea --map_yaml /path/to/map.yaml
The .yaml's image: path is resolved relative to the yaml file, so keep the
pair together. Pass --map_yaml "" to ignore the yaml and drive the map from
--map_image + the individual --map_* flags instead.
- World origin
(0, 0)is the centre of the map — the exported yamloriginis the bottom-left corner[-w/2, -h/2, 0], which places it centred. xpoints east (right),ypoints north (up), matching the simulator.- PNG row 0 is the north edge; the sim loads with
flipY = trueso image row 0 becomes the bottom (south) row of the world.
Design and export these maps in the browser with the Map Editor.