Two calls, for two purposes.
On the leader the second one is called get_stats(). Same shape, same contract.

metrics()

The quick check. Everything in it is a number you can print each second.

stats()

The full report. A flat list, one entry per subject: the link, the control loop, each joint, each camera, the recorder and the session as a whole. A leader reports its own half, including video sync and how long a frame takes to reach the screen. Every entry is shaped like WebRTC’s getStats(), so a collector you already run can scrape it unchanged. Counters are cumulative and never reset, so take two snapshots and subtract to get a rate. Gauges end in Current. Print one to see what your own system reports:
That is the reliable way to find a field name. The report describes the machine in front of you, including one entry per joint and per camera you actually have.

What to look for

  • Is the link healthy. Round trip time, packet loss, and whether the send buffer is growing. A buffer that climbs means the channel cannot keep up with what you are sending.
  • Is the loop keeping its period. Tick jitter and overruns. Rising overruns mean your reads or writes are too slow for the rate you asked for.
  • Is the arm tracking. Per joint, the error between where it was told to go and where it got to, alongside how often the speed cap fired. Both large together means the speed limit is what is holding the arm back.
  • Is it acting on fresh data. How old an observation was by the time the robot acted on it. This is the number that matters most.
  • Is the correlation good. How many commands arrived with a usable observation id. The rest become guessed rows at export time, see Exporting datasets.
  • Is the recording keeping up. Queue depth against capacity, and how many rows were dropped.

Why there is no end-to-end latency

Nothing here reports operator-to-motor time, and that is deliberate. It spans two machines with no shared clock, so producing a number would mean subtracting two wall clocks, which is not a latency. What the SDK reports as fact is measured on a single machine, which is why observation-to-applied is exact and anything crossing the network is an estimate.
Keep to the same rule if you build your own timing on top of this. Two wall clocks from different machines do not subtract into a latency.
On a leader that only receives, most link measurements are absent or None. A round trip time exists only for a stream you send. Absent is not zero, so render it as unknown rather than as healthy.

Watching the other end

Each side publishes its report to the other at stats_hz, default 1 Hz.
remote_stats is [] when nothing has arrived, which is a real state and not an error: the far side may have stats_hz=0. A climbing remote_stats_age_ms is the other machine going quiet, which the counters inside the report cannot show because they simply stop. Set stats_hz=0 to stop publishing. stats() and get_stats() still work.

Dashboard

A live page, served in process off a follower or a leader.
str
default:"127.0.0.1"
0.0.0.0 to reach a headless robot host from another machine on the LAN.
int
default:"8080"
0 picks a free one, which you read back off .url.
float
default:"2.0"
How often it samples stats.
float
default:"10.0"
Camera encode rate for the grid. 0 serves stats with no pictures, for a host with no CPU to spare.
For a program that should still run when the port is taken, use serve(), which degrades to doing nothing instead of raising:
A falsy port disables it, so you never branch on whether the flag was passed.
On ROS 2 the dashboard is a bridge parameter rather than a call. See Reference.

Troubleshooting

Symptoms, and which of these numbers to read first.

Follower

Where these calls live, and the rest of the class.