Services API Reference¶
The DAM API server exposes REST endpoints and a WebSocket stream.
Start the server:
uvicorn dam.services.api:app --host 0.0.0.0 --port 8080
# Interactive docs: http://localhost:8080/docs
Telemetry¶
GET /api/telemetry/history¶
Return the last n cycle events from the ring buffer.
| Parameter | Type | Default | Description |
|---|---|---|---|
n |
int | 50 | Number of events (1–1000) |
WS /ws/telemetry¶
Real-time WebSocket stream. Replays last 50 events on connect,
then pushes every new CycleResult as it arrives.
Wiring TelemetryService with MetricBus¶
Pass the runtime's MetricBus and the cycle budget when constructing
TelemetryService to enable the perf field on every event:
from dam.services.telemetry import TelemetryService
telemetry = TelemetryService(
metric_bus=runtime.metric_bus,
cycle_budget_ms=1000.0 / runtime.control_frequency_hz,
)
When metric_bus is omitted (the default), the service behaves exactly as
before and the perf key is absent from all events.
Event shape¶
Each message is a JSON object with "type": "cycle".
The perf field is present only when MetricBus is wired in (see above).
{
"type": "cycle",
"cycle_id": 42,
"trace_id": "3fa8b1c2-...",
"was_rejected": false,
"was_clamped": false,
"risk_level": "NORMAL",
"fallback_triggered": null,
"latency_ms": {
"obs": 0.8,
"policy": 2.1,
"validate": 5.4,
"sink": 0.4,
"total": 8.7
},
"guard_statuses": [
{ "name": "OODGuard", "layer": "L0", "decision": "PASS", "reason": "" },
{ "name": "MotionGuard", "layer": "L2", "decision": "PASS", "reason": "" }
],
"active_task": "pick",
"active_boundaries": ["workspace", "approach"],
"active_cameras": ["top", "wrist"],
"timestamp": 1700000000.123,
"perf": {
"stages": {
"source": 0.8,
"policy": 2.1,
"guards": 5.4,
"sink": 0.4,
"total": 8.7
},
"layers": {
"L0": 2.1,
"L2": 2.0,
"L3": 1.3
},
"guards": {
"OODGuard": 2.1,
"MotionGuard": 1.0,
"WorkspaceGuard": 1.0,
"HardwareGuard": 1.3
},
"deadline_ms": 20.0,
"slack_ms": 11.3
}
}
perf field |
Type | Description |
|---|---|---|
stages.source |
float ms | Sensor read (source.read()) |
stages.policy |
float ms | Policy inference (policy.predict()) |
stages.guards |
float ms | All guards combined (validate()) |
stages.sink |
float ms | Action dispatch (sink.apply()) |
stages.total |
float ms | End-to-end cycle time |
layers.* |
float ms | Sum of guard latencies for that GuardLayer in this cycle.Keys follow the "L0"–"L3" convention; only layers that executed appear. |
guards.* |
float ms | Per-guard latest execution time. |
deadline_ms |
float ms | Configured cycle budget (1000 / control_frequency_hz). |
slack_ms |
float ms | Remaining headroom: deadline_ms − total_ms. Negative means over-budget. |
Binary Message Protocol¶
To avoid Base64 encoding overhead (which adds ~33% bandwidth and significant CPU latency), live camera frames are pushed as raw binary messages over the same WebSocket connection immediately following a cycle JSON event.
Connected clients should set ws.binaryType = "arraybuffer" and parse binary messages using the following protocol:
| Byte Offset | Size | Name | Description |
|---|---|---|---|
| 0 | 1 byte | Magic | Protocol version identifier. Fixed: 0x01 |
| 1 | 1 byte | Name Length (L) | Length of the camera name string in bytes. |
| 2 | L bytes | Camera Name | UTF-8 encoded camera name (e.g., top). |
| 2 + L | variable | JPEG Payload | Raw binary JPEG data. |
Frontends can render these efficiently using URL.createObjectURL(new Blob([jpegData], {type: 'image/jpeg'})).
A {"type":"ping"} keepalive is sent every 30 s.
Risk Log¶
GET /api/risk-log¶
Query historical risk events.
| Parameter | Type | Description |
|---|---|---|
since |
float | Unix timestamp lower bound |
until |
float | Unix timestamp upper bound |
min_risk_level |
str | NORMAL · ELEVATED · CRITICAL · EMERGENCY |
outcome |
string | Filter canonical cycle outcome: reject, clamp, or pass |
rejected_only |
bool | Backward-compatible alias for outcome=reject |
clamped_only |
bool | Backward-compatible alias for outcome=clamp |
limit |
int | Max events (default 100, max 5000) |
GET /api/risk-log/stats¶
Summary statistics: total events, rejected, clamped, by risk level, avg latency.
GET /api/risk-log/mcap/{filename}¶
Read notable cycles from a recorded MCAP session through the same event
contract used by Risk Log and Cycle Inspector. Supports min_risk_level,
outcome, failure_type, and limit; outcomes use the canonical lowercase
values reject, clamp, and pass.
GET /api/risk-log/export/json¶
Download all events as risk_log.json.
GET /api/risk-log/export/csv¶
Download all events as risk_log.csv.
GET /api/risk-log/{event_id}¶
Get a single event by its integer ID.
Boundaries¶
GET /api/boundaries¶
List all boundary configs.
GET /api/boundaries/{name}¶
Get a single boundary config.
POST /api/boundaries¶
Create a new boundary config. Body: boundary config JSON.
PUT /api/boundaries/{name}¶
Replace a boundary config.
DELETE /api/boundaries/{name}¶
Delete a boundary config.
Runtime Control¶
GET /api/control/status¶
States: idle · running · paused · stopped · emergency
POST /api/control/start¶
| Parameter | Type | Default | Description |
|---|---|---|---|
task_name |
str | default |
Task name to activate |
n_cycles |
int | -1 | Cycles to run (-1 = run forever) |
cycle_budget_ms |
float | 20.0 | Target cycle time in ms |
POST /api/control/pause¶
Pause after the current cycle completes.
POST /api/control/resume¶
Resume a paused runtime.
POST /api/control/stop¶
Graceful stop.
POST /api/control/estop¶
Immediate emergency stop. Also calls sink.emergency_stop() if available.
POST /api/control/reset¶
Reset to idle (only from stopped or emergency).
Loopback Sessions¶
Note
These endpoints are available only if loopback.backend = "mcap" is set in the Stackfile
and at least one cycle has been written.
GET /mcap/sessions¶
List all MCAP session files in the configured output_dir.
Response:
{
"sessions": [
{
"session_id": "sess_20241210_143022_abc123",
"filename": "session_20241210_143022_abc123.mcap",
"size_mb": 123.4,
"created_at": 1700000000.123,
"rotated_at": 1700003600.456,
"file_count": 3,
"total_cycles": 180000,
"violation_cycles": 5,
"clamp_cycles": 12,
"has_images": true
}
]
}
GET /mcap/sessions/{session_id}¶
Metadata for a specific session (MCAP header only; does not read entire file).
Response:
{
"session_id": "sess_20241210_143022_abc123",
"filename": "session_20241210_143022_abc123.mcap",
"size_mb": 123.4,
"start_time": 1700000000.123,
"end_time": 1700003600.789,
"total_cycles": 180000,
"violation_cycles": 5,
"clamp_cycles": 12,
"has_images": true,
"compression": "zstd",
"channels": ["/dam/cycle", "/dam/obs", "/dam/action", "/dam/L0", "/dam/L2", "/dam/images/camera0"]
}
GET /mcap/sessions/{session_id}/download¶
Download (a subset of) the session file.
| Parameter | Type | Default | Description |
|---|---|---|---|
start_cycle |
int | 0 | First cycle to include |
end_cycle |
int | -1 | Last cycle to include (-1 = all) |
topics |
str | (all) | Comma-separated channel list (e.g. /dam/cycle,/dam/L2) |
Example:
# Download cycles 100–200, only /dam/cycle and /dam/L2
curl 'http://localhost:8080/mcap/sessions/sess_20241210.../download?start_cycle=100&end_cycle=200&topics=/dam/cycle,/dam/L2' \
-o incident.mcap
Response: Binary MCAP file with Content-Disposition attachment.