Skip to content

Capability 005: Telemetry

Status: Accepted

Context

The Robot Operator drives the robot with the Web App but does not see how it responds: whether the motors turn as commanded and how much charge the battery has left. The Drive reports both on the CAN bus, the Superordinate Controller receives them as > lines of the CAN Adapter since capability 004.

With this capability, the Superordinate Controller keeps the latest value of each quantity and pushes a Telemetry Snapshot to every connected client at a fixed interval. A client sees the current state, not every message: the Drive sends 300 MotorNSpeed messages per second, a human reads at most a few values per second.

References

Details

Endpoint

Method Path Request body Success
GET /v1/telemetry none 200 OK, text/event-stream

Telemetry Stream

The response is an endless stream of Server-Sent Events. Each event has the event type telemetry and one Telemetry Snapshot as JSON in its data:

event: telemetry
data: {"type":"telemetry","version":1,"timestamp":"2026-10-09T12:00:00.000Z","source":"superordinate-controller","batteryVoltage":36.25,"motorSpeeds":[50,50,100]}
Field Type Meaning
type string Always telemetry
version integer Version of the snapshot format, 1
timestamp string Time of the snapshot, ISO 8601 in UTC with milliseconds
source string Always superordinate-controller
batteryVoltage number or null Battery Voltage in volts, two decimals; null while it is not current
motorSpeeds array of 3 Motor Speed of motors 1 to 3 in percent, integer from -100 to 100; an entry is null while it is not current

A value is current while its last CAN message is at most 3 s old.

Received CAN Messages

CAN message > line identifier Taken for
Motor1Status 0C210003 Battery Voltage of board 1
Motor2Status 0C220003 Nothing: it carries the same values as Motor1Status
Motor3Status 0C230003 Battery Voltage of board 2
Motor1Speed 0C210004 Motor Speed of motor 1
Motor2Speed 0C220004 Motor Speed of motor 2
Motor3Speed 0C230004 Motor Speed of motor 3

Battery Voltage

The Battery Voltage is the mean of the Supply Voltages of board 1 and board 2 that are current. If only one is current, it is that one; if none is, it is null.

Motor Speed

For motor \(i\) with the Measured Speed \(n_i\) and the reference \(n_{max}\) of MOTOR_SPEED_AT_FULL_SCALE, the Motor Speed in percent is

\[p_i = \operatorname{clamp}\left(\operatorname{round}\left(100 \cdot \sigma_i \cdot \frac{n_i}{n_{max}}\right), -100, 100\right)\]

rounded half away from zero, with \(\sigma_1 = \sigma_3 = 1\) and \(\sigma_2 = -1\). The sign of \(p_i\) is the sign of the target speed that turns motor \(i\) in this direction.

Test vectors with \(n_{max} = 1000\):

Motor > line \(p_i\)
1 >0C210004:F4 01 (500) 50
2 >0C220004:0C FE (-500) 50
3 >0C230004:4C 04 (1100) 100
1 >0C210004:05 00 (5) 1
1 >0C210004:FC FF (-4) 0

Environment Variables

Variable Meaning Default Valid values
MOTOR_SPEED_AT_FULL_SCALE Measured Speed that corresponds to 100 % 1000 Integer from 1 to 32767
TELEMETRY_INTERVAL Milliseconds between two Telemetry Snapshots 1000 Integer from 100 to 10000

Functional Requirements

  1. The Superordinate Controller takes the CAN messages of the table under Received CAN Messages from the > lines of the CAN Adapter and keeps the latest value of each, with its time of receipt.
  2. A client opens the Telemetry Stream with GET /v1/telemetry. The Superordinate Controller sends a Telemetry Snapshot at once and then every TELEMETRY_INTERVAL milliseconds, until the client closes the connection.
  3. Each Telemetry Snapshot carries the values as computed under Battery Voltage and Motor Speed at the time of the snapshot.
  4. Several clients may hold a Telemetry Stream at once, at most 4.

Non-Functional Requirements

  1. The first Telemetry Snapshot arrives at most 100 ms after the request.
  2. Two consecutive Telemetry Snapshots are TELEMETRY_INTERVAL ± 100 ms apart.
  3. A value received on the pseudo-terminal appears in the next Telemetry Snapshot but one at the latest.
  4. With 4 open Telemetry Streams and 300 MotorNSpeed and 3 MotorNStatus messages per second on the pseudo-terminal, Non-Functional Requirement 1 of capability 002 still holds.

Technical Requirements

  1. The Telemetry Stream is served by cpp-httplib like the other endpoints, with the CORS handling of capability 002.

Error Handling

  1. A fifth request for a Telemetry Stream is answered with 503 Service Unavailable and an application/problem+json body, as for capability 002, Error Handling 4.
  2. A > line with an identifier of the table under Received CAN Messages whose payload has fewer than the bytes of its message is logged like an error line of capability 004, at most once per second, and does not change the kept value.
  3. A > line with any other identifier is ignored.
  4. A client that closes its Telemetry Stream ends it without an error log.
  5. An invalid value of MOTOR_SPEED_AT_FULL_SCALE or TELEMETRY_INTERVAL is logged, naming the variable, and the Superordinate Controller exits with a non-zero status before it serves, as for capability 004, Error Handling 2.

Out of Scope

  • Other values of the Drive: Controller Temperature, MotorNState, and the right output of board 2
  • Minimum, maximum, or mean of a value over the interval
  • Pushing an event at once when a value changes or an error occurs
  • Telemetry of other subsystems, such as battery management and distance sensors
  • Showing the telemetry in the Web App

Acceptance Criteria

All criteria except the last run against a simulated CAN Adapter on a pseudo-terminal.

  1. The simulated adapter sends >0C210003:10 0E 1E 00 (36.00 V) and >0C230003:42 0E 1F 00 (36.50 V), and the > lines of the first three test vectors under Motor Speed: a client opens the Telemetry Stream and receives, within 100 ms, a Telemetry Snapshot with batteryVoltage 36.25 and motorSpeeds [50, 50, 100], the response allowed for the CORS origin of capability 002.
  2. Each test vector under Motor Speed yields its \(p_i\) in the next Telemetry Snapshot but one.
  3. Started with TELEMETRY_INTERVAL=200: the Telemetry Snapshots arrive 200 ± 100 ms apart.
  4. Motor1Status keeps arriving, Motor3Status arrived last 4 s ago: batteryVoltage is that of Motor1Status. No message for 4 s: batteryVoltage and every entry of motorSpeeds are null.
  5. Started with MOTOR_SPEED_AT_FULL_SCALE=500: >0C210004:F4 01 (500) yields 100 for motor 1.
  6. A > line >0C210004:F4 with too short a payload: logged, and motorSpeeds keeps the previous entry of motor 1.
  7. 4 clients hold a Telemetry Stream: a fifth is answered with 503 and an application/problem+json body; after one client closes its stream, a new one is served.
  8. 4 open Telemetry Streams and the load of Non-Functional Requirement 4: Remote Drive Commands every 250 ms for 3 s meet Non-Functional Requirement 1 of capability 002.
  9. Started with an invalid value, e.g. MOTOR_SPEED_AT_FULL_SCALE=0, =abc, or TELEMETRY_INTERVAL=50: exits with a non-zero status and a log message naming the variable.
  10. On the robot, lifted with free wheels: a rotation at speed 50 shows about -50 % on all three motors; a translation in direction 90 at speed 50 shows about -25 %, 50 %, and -25 %; the Battery Voltage matches a multimeter on the battery within 0.5 V (manual test test/manual/robot.md).

Decisions

  1. The Superordinate Controller pushes the latest state at a fixed interval, by Server-Sent Events. A client needs the current state, not every message; Server-Sent Events push in one direction over plain HTTP with the CORS of capability 002, and a browser reconnects by itself. The fixed interval also tells the client that the stream is alive. Consequence: a short peak between two snapshots is not shown; each open stream holds a worker thread of cpp-httplib, hence the limit of 4.
  2. The Superordinate Controller converts to volts and percent and flips the sign of motor 2. It knows the Drive: the boards report the Measured Speed of the right output with the opposite sign of its command, and the Motor Controller Bridge passes values on unchanged. A client only shows the values. Consequence: the sign of motor 2 rests on the firmware of the boards; the manual test on the robot verifies it.
  3. A value older than 3 s is null, not its last value. An old value shown as current would hide a silent board; 3 s are three missed MotorNStatus messages. One limit for all values keeps the rule simple. Consequence: a Motor Speed stays shown for up to 3 s after its board falls silent.
  4. The snapshot carries the message envelope without correlationId. A snapshot answers no request and follows from no other message, so there is nothing to correlate. Consequence: this deviates from the envelope of the API design guideline.