Signal profile reference
Authoritative reference for the dynamic signal profile extension to MQTT Status Displays. The byte-level wire format and the device admission order live in the repository specification docs/specifications/mqtt-signal-profiles-v1.md.
Terms
- Signal Pattern — a bounded program of timed RGB, white, and CCT output for one semantic display status.
- Signal Profile Revision — an immutable compiled contract that binds the nine display statuses to patterns. Authoring names and unused library patterns do not enter the compiled bytes.
- Profile Activation — the desired assignment of one revision to a Status Display within its Display Binding, ordered by a monotonic activation generation.
- Admission fence — the controller's durable record of the highest admitted generation, revision ID, and hash. An older backup cannot lower it.
Status binding order
The compiled header binds nine statuses in a fixed order. Raw Printer error codes are classifier inputs, not profile statuses.
| Index | Status | Meaning |
|---|---|---|
0 | FILAMENT_RUNOUT | Needs filament |
1 | ERROR | Needs help |
2 | COMPLETE | Recent completion |
3 | OFFLINE | Explicitly offline |
4 | PAUSED | Paused |
5 | RUNNING | Printing |
6 | WAITING | Waiting for work |
7 | IDLE | Idle |
8 | UNKNOWN | Reserved safety state |
Authoring draft
The editable draft is JSON. Channels and the brightness cap are integers from 0 through 255. Durations are milliseconds and must be multiples of 50. CCT is a warm-to-cool mix coordinate, not a Kelvin value.
{
"name": "Print farm signal library",
"brightnessCap": 180,
"patterns": [
{
"id": "status_running", // [A-Za-z0-9_-], 1-64 chars
"name": "Printing",
"program": {
"initial": { "red": 26, "green": 220, "blue": 118,
"white": 0, "cct": 0 },
"phases": [
{ "durationMs": 600,
"target": { "red": 0, "green": 0, "blue": 0,
"white": 0, "cct": 0 },
"transition": "linear" } // "step" | "linear"
],
"traversals": 0 // 0 = continuous, else 1-255
}
}
],
"bindings": {
"FILAMENT_RUNOUT": "status_filament_runout",
"ERROR": "status_error",
"COMPLETE": "status_complete",
"OFFLINE": "status_offline",
"PAUSED": "status_paused",
"RUNNING": "status_running",
"WAITING": "status_waiting",
"IDLE": "status_idle",
"UNKNOWN": "status_unknown"
},
"stale": { "mode": "first_pixel", "pattern": "status_stale" }
}A draft holds at most 64 library patterns. Only patterns referenced by a status binding or the stale binding compile. Validation rules — phase count, cycle length, repeating-program closure, constant CCT, the COMPLETE restriction, and the byte-identity guard on UNKNOWN and stale — are listed in Configure a custom signal palette.
Compiled format summary
The compiler emits canonical PHLP version 1 bytes: unsigned little-endian integers, no padding, no strings. The 20-byte header carries the format version, 50 ms tick, pattern count (1–10), status count (9), brightness cap, nine binding bytes, the stale mode, and the stale pattern binding. Each pattern record carries its ID, phase count (0–8), traversal count, five initial channel bytes, and 8-byte phases. Byte-identical programs deduplicate; pattern IDs are assigned by lexicographic program order, so names and authoring order cannot change the result.
- Maximum decoded size: 750 bytes; minimum 29 bytes.
- Standard padded base64 encoding: at most 1,000 characters.
- The complete UTF-8
profile/configenvelope is at most 4,096 bytes. - SHA-256 over the canonical binary is the content identity. It is not authentication; TLS, topic ACLs, and the Display Binding authorize delivery.
REST API
All routes sit under https://api.printhiv3d.com/v1 and use the standard JWT or API-key authentication with the x-organization-id header. List and read routes accept any organization member; every mutation requires an organization admin (403 ORG_ADMIN_REQUIRED otherwise). Request bodies are validated strictly — unknown fields return 400 MQTT_DISPLAY_REQUEST_INVALID.
| Method and path | Body | Returns |
|---|---|---|
GET /mqtt/profiles | — | SignalProfileSummary[] |
POST /mqtt/profiles | { name } | SignalProfileDetail (seeds the default draft) |
POST /mqtt/profiles/compile | { draft } | { byteLength, sha256, data, capabilityRequirements } |
GET /mqtt/profiles/{id} | — | SignalProfileDetail |
PATCH /mqtt/profiles/{id} | { name?, archived?, expectedRevision } | SignalProfileDetail |
PUT /mqtt/profiles/{id}/draft | { draft, expectedRevision } | SignalProfileDetail |
POST /mqtt/profiles/{id}/publish | { expectedRevision } | SignalProfileRevision |
GET /mqtt/profiles/{id}/revisions | — | SignalProfileRevision[] |
GET /mqtt/revisions/{revisionId} | — | SignalProfileRevision |
GET /mqtt/profile-targets | — | DesiredSignalProfileActivation[] |
GET /mqtt/displays/{id}/profile | — | DesiredSignalProfileActivation |
POST /mqtt/displays/{id}/profile | { profileRevisionId, expectedBindingEpoch, expectedActivationGeneration } | DesiredSignalProfileActivation |
POST /mqtt/displays/{id}/profile/retry | — | DesiredSignalProfileActivation |
Response objects
SignalProfileSummary: id, name, draftRevision, latestPublishedRevision (null before the first publish), archived, updatedAt. SignalProfileDetail adds the draft document.
SignalProfileRevision: id, profileId, revision, authoringSnapshot, compilerVersion, byteLength, sha256, base64 data, capabilityRequirements, publishedBy, publishedAt. Revisions are immutable at the database layer.
DesiredSignalProfileActivation: displayId, bindingEpoch, desired and applied activation identities (activation_generation, profile_revision_id, sha256; applied is null until a matching receipt), status, errorCode, updatedAt. status is sync_pending, device_applied, or rejected; device_applied requires the applied identity to equal the desired identity exactly.
Concurrency and conflict errors
Mutations are optimistic-concurrency checked. expectedRevision fences draft saves, renames, archives, and publishes against the current draftRevision. expectedBindingEpoch and expectedActivationGeneration fence assignments against the display's current binding and generation.
| HTTP | Code | Cause |
|---|---|---|
404 | signal_profile_not_found / signal_profile_revision_not_found / signal_profile_target_not_found / display_not_found | The addressed object does not exist in this organization |
409 | signal_profile_revision_conflict | expectedRevision does not match the current draft revision |
409 | signal_profile_archived | Draft save or publish on an archived profile |
409 | display_binding_epoch_conflict | The display was rebound; expectedBindingEpoch is stale |
409 | signal_profile_activation_conflict | expectedActivationGeneration does not match the current generation |
409 | signal_profile_capability_missing | The controller has not advertised the signal_profile capability |
409 | signal_profile_capability_insufficient | The revision exceeds the advertised size, phase, white, or CCT limits |
409 | signal_profile_target_healthy | Retry requested on a target with no recorded error |
503 | MQTT_DISPLAY_NOT_CONFIGURED | The deployment lacks display database or secret configuration |
MQTT topics
All display topics share the binding-scoped base ph/v1/{organizationId}/display/{displayId}/b/{bindingEpoch}. The profile extension adds two topics:
| Topic | Direction | Delivery |
|---|---|---|
{base}/profile/config | Cloud → display | Retained, QoS 1. One complete profile envelope per activation. |
{base}/credential/{credentialId}/profile/status | Display → cloud | QoS 1, non-retained. Sent on apply, reject, defer, and after reconnect. |
Mosquitto Dynamic Security scopes each display credential to subscribe to its own display's topics — including profile/config — and to publish only on its credential-bound health, capabilities, resync, and profile status paths. The server publisher principal is scoped to ph/v1/{organizationId}/display/#. Rebind cleanup and retained-topic cleanup cover these paths; a credential can never reach a sibling display.
profile/config envelope
Strict JSON; unknown fields are rejected.
| Field | Type | Meaning |
|---|---|---|
profile_sync_version | 1 | Envelope schema version |
organization_id | UUID | Owning organization |
display_id | UUID | Target Status Display |
binding_epoch | integer ≥ 1 | Display Binding epoch |
activation_generation | integer ≥ 1 | Monotonic generation within the binding |
profile_revision_id | UUID | Desired Signal Profile Revision |
format_version | 1 | Compiled profile format version |
byte_length | 29–750 | Exact decoded byte count |
sha256 | 64 lowercase hex | SHA-256 of the canonical binary |
created_at | RFC3339 UTC | Envelope creation time |
expires_at | RFC3339 UTC | Transport deadline; at most seven days after created_at |
data | base64 ≤ 1000 chars | The compiled profile |
The service currently publishes each envelope with a three-day transport deadline. An expired envelope cannot authorize a new activation; a device that holds one reports refresh_requested and the cloud republishes the same activation with fresh timestamps.
profile/status receipt
Strict JSON, at most 2,048 UTF-8 bytes. The envelope carries organization_id, display_id, binding_epoch, credential_id, session_id, session_generation, an increasing status_sequence within the session, observed_at, and refresh_requested. The profile fields:
| Field | Meaning |
|---|---|
active | The actual active activation identity, or null when no valid profile is installed |
attempted | The candidate identity this result concerns; null for an unsolicited announcement |
highest_admitted | The durable admission-fence identity; null only before the first admission in a new binding |
result | applied | rejected | deferred | announced |
refresh_requested | True when the retained desired envelope needs a fresh transport deadline |
error_code | One bounded error code, or null |
Receipt fencing: the API accepts a receipt only from the current credential and session under the existing session-fencing rules, and only with a newer status_sequence. An applied result requires active and highest_admitted to equal the attempted identity exactly. A rejection never represents an applied candidate, and an older receipt cannot satisfy a newer desired activation.
Generations and the admission fence
- The activation generation is a positive safe integer, monotonic per display within its binding. It orders assignments across different profile libraries, so rollback is a newer generation that references older bytes.
- The device rejects generations older than its durable admission-fence identity and rejects a reused generation with a different revision or hash.
- The fence is written after the candidate bytes verify and before the profile activates. A failed fence write cannot activate the candidate.
- The fence recovers independently of profile blob selection. If no trustworthy fence can be recovered, the device refuses profile activation until a new Display Binding is established.
Stale modes
| Value | Draft name | Behavior while the feed is stale |
|---|---|---|
0 | per_pixel | Each pixel renders its UNKNOWN binding |
1 | all_pixels | Every mapped pixel renders the stale program |
2 | first_pixel | Logical pixel 1 renders the stale program; every other pixel is off |
The stale trigger is feed-level evidence failure: source expiry, transport expiry, or untrusted timestamps. The stale binding must name an included pattern in every mode, and the stale program cannot be byte-identical to a program bound to FILAMENT_RUNOUT through WAITING.
Capability advertisement
A controller advertises dynamic profile support inside its capabilities report. The API checks these fields against each revision's capabilityRequirements before an assignment is accepted:
| Field | Meaning |
|---|---|
format_version | Compiled format the decoder supports (1) |
max_decoded_bytes | Decoder allocation bound (29–750) |
max_phases | Maximum phases per pattern (0–8) |
supports_white_output | White channel is wired and supported |
supports_cct | CCT channel is wired and supported |
Firmware without this capability keeps its built-in patterns. The API rejects the assignment with signal_profile_capability_missing and no profile message is published.
Delivery behavior
- Each desired activation is persisted in PostgreSQL before publication, then published retained at QoS 1. At most one new activation publishes per display per second; retries coalesce to the newest desired activation.
- While a receipt is outstanding the worker republishes on a bounded schedule (about 45 seconds), refreshing the transport deadline with the same activation identity.
- Publication failures back off exponentially to a 60-second ceiling. After 20 attempts the target records the error and stops; use the retry route to resume.
- A broker acknowledgment means delivered to the broker — not applied on the device. Only a device receipt moves the target to
device_applied. - A server restart rebuilds the retained desired configuration from PostgreSQL, not from the broker.
Device receipt error codes
profile_fence_recovery_required, stale_activation, activation_conflict, hash_mismatch, unsupported_profile_format, profile_too_large, invalid_program, unsupported_white_output, profile_persist_failed, and completion_cache_busy (returned with a deferred result). See Retry a rejected profile activation for the operator action for each code.