-
Notifications
You must be signed in to change notification settings - Fork 16
Description
Summary
Triggers are event-based notifications that fire when a condition is met on an observed resource. Unlike cyclic subscriptions (which push data at fixed intervals regardless of changes), triggers only fire when something specific happens - a value changes, reaches a target, enters a range, or leaves a range.
Triggers deliver events via SSE, like cyclic subscriptions.
Proposed solution
1. POST /api/v1/{entity-path}/triggers
Create a new trigger on an observed resource.
Applies to entity types: Components, Apps
Path parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
{entity-path} |
URL segment | Yes | e.g., apps/temp_sensor |
Request body:
{
"resource": "/api/v1/apps/temp_sensor/data/temperature",
"trigger_condition": {
"condition_type": "LeaveRange",
"lower_bound": 20.0,
"upper_bound": 30.0
},
"path": "/data",
"protocol": "sse",
"multishot": true,
"lifetime": 3600
}| Field | Type | Required | Description |
|---|---|---|---|
resource |
string (URI) |
Yes | Full URI of the resource to observe |
trigger_condition |
TriggerCondition |
Yes | Condition that fires the trigger (see below) |
path |
string (JSON Pointer) |
No | Sub-element within the resource to observe (e.g., /data to watch the value field). If omitted, the entire resource is observed. |
protocol |
string |
No | Transport protocol. Only "sse" is supported. Default: "sse". |
multishot |
boolean |
No | If true, trigger fires on every condition match. If false (default), trigger fires once then auto-terminates. |
persistent |
boolean |
No | If true, trigger survives server restart (stored persistently). Default: false. |
lifetime |
integer |
No | Seconds until auto-termination. If omitted, trigger lives until manually deleted or server restarts (for non-persistent). |
log_settings |
object |
No | Auto-logging configuration when trigger fires (severity, marker text). |
Trigger Conditions
The trigger_condition object has a condition_type discriminator:
OnChange - fires on any value change
{
"condition_type": "OnChange"
}No additional fields. Fires whenever the observed value differs from the previous reading.
OnChangeTo - fires when value changes to a specific target
{
"condition_type": "OnChangeTo",
"target_value": 100.0
}| Field | Type | Description |
|---|---|---|
target_value |
any | The target value to match |
EnterRange - fires when value enters a range
{
"condition_type": "EnterRange",
"lower_bound": 20.0,
"upper_bound": 30.0
}| Field | Type | Description |
|---|---|---|
lower_bound |
number | Lower boundary (inclusive) |
upper_bound |
number | Upper boundary (inclusive) |
Fires when the value transitions from outside [lower_bound, upper_bound] to inside.
LeaveRange - fires when value leaves a range
{
"condition_type": "LeaveRange",
"lower_bound": 20.0,
"upper_bound": 30.0
}Fires when the value transitions from inside [lower_bound, upper_bound] to outside.
Response 201 Created:
{
"id": "trig_001",
"status": "active",
"observed_resource": "/api/v1/apps/temp_sensor/data/temperature",
"event_source": "/api/v1/apps/temp_sensor/triggers/trig_001/events",
"trigger_condition": {
"condition_type": "LeaveRange",
"lower_bound": 20.0,
"upper_bound": 30.0
}
}| Field | Type | Description |
|---|---|---|
id |
string |
Server-generated trigger identifier |
status |
string |
active or terminated |
observed_resource |
string (URI) |
The resource being observed |
event_source |
string (URI) |
URI to connect to for receiving trigger events |
trigger_condition |
TriggerCondition |
The condition being evaluated |
Error responses:
| Status | Error Code | When |
|---|---|---|
400 |
invalid-parameter |
Invalid resource URI, unknown condition type, missing required fields, lower_bound > upper_bound |
404 |
entity-not-found |
Entity doesn't exist |
501 |
not-implemented |
Feature not implemented |
503 |
service-unavailable |
Server is at capacity |
2. GET /api/v1/{entity-path}/triggers
List all triggers for an entity.
Response 200 OK:
{
"items": [
{
"id": "trig_001",
"status": "active",
"observed_resource": "/api/v1/apps/temp_sensor/data/temperature",
"event_source": "/api/v1/apps/temp_sensor/triggers/trig_001/events",
"trigger_condition": { "condition_type": "OnChange" }
}
]
}3. GET /api/v1/{entity-path}/triggers/{id}
Read a single trigger's details.
Response 200 OK: Returns a single Trigger object.
Error: 404 if trigger doesn't exist.
4. PUT /api/v1/{entity-path}/triggers/{id}
Update the lifetime of an existing trigger.
Request body:
{
"lifetime": 7200
}Response 200 OK: Returns the updated Trigger.
Error: 404 if trigger doesn't exist, 400 if invalid lifetime.
5. DELETE /api/v1/{entity-path}/triggers/{id}
Remove a trigger. Closes the SSE event stream if connected.
Response 204 No Content
Error: 404 if trigger doesn't exist.
6. GET /api/v1/{entity-path}/triggers/{id}/events
SSE event stream that delivers trigger events.
Event format (EventEnvelope):
data: {"timestamp":"2026-02-14T10:30:00Z","payload":{"id":"temperature","data":{"data":35.2}}}
For single-shot triggers (multishot: false): one event is sent, then the stream closes and the trigger status transitions to terminated.
For multi-shot triggers: events continue until the trigger is deleted, its lifetime expires, or the server restarts.
Observable Resources
Triggers can observe these resource types:
data- topic values (most common)faults- fault status changesoperation executions- execution status changesscript executions- script execution status changesupdates- software update status changeslocks- lock state changes
Business Rules
- Persistent triggers cannot be created on resources that require a lock - return
400if attempted - Trigger status transitions:
active→terminated(single-shot after firing, lifetime expiry, or manual delete) log_settingsis optional - if provided, the server automatically creates a log entry when the trigger fires
Additional context
Architecture
- Create a
TriggerManagerclass that stores trigger definitions and evaluates conditions - Subscribe to the observed ROS 2 topic on trigger creation
- On each message: extract the value at
path(JSON pointer), compare with previous value, evaluate condition - If condition fires: push EventEnvelope via SSE, handle single-shot termination
Reuse existing SSE infrastructure
Same SSE pattern as SSEFaultHandler and cyclic subscriptions. Extract common utilities.
Route registration
srv->Post((api_path("/apps") + R"(/([^/]+)/triggers$)"), handler);
srv->Get((api_path("/apps") + R"(/([^/]+)/triggers$)"), handler);
srv->Get((api_path("/apps") + R"(/([^/]+)/triggers/([^/]+)$)"), handler);
srv->Put((api_path("/apps") + R"(/([^/]+)/triggers/([^/]+)$)"), handler);
srv->Delete((api_path("/apps") + R"(/([^/]+)/triggers/([^/]+)$)"), handler);
srv->Get((api_path("/apps") + R"(/([^/]+)/triggers/([^/]+)/events$)"), handler);
// Same for /components/Tests
- Unit test: create OnChange trigger → 201
- Unit test: create EnterRange trigger with bounds → 201
- Unit test: invalid condition type → 400
- Unit test: lower_bound > upper_bound → 400
- Unit test: single-shot trigger fires once then terminates
- Unit test: multi-shot trigger fires repeatedly
- Unit test: lifetime expiry auto-terminates
- Integration test: create trigger on live topic, verify events fire on value change