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.

IndexStatusMeaning
0FILAMENT_RUNOUTNeeds filament
1ERRORNeeds help
2COMPLETERecent completion
3OFFLINEExplicitly offline
4PAUSEDPaused
5RUNNINGPrinting
6WAITINGWaiting for work
7IDLEIdle
8UNKNOWNReserved 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/config envelope 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 pathBodyReturns
GET /mqtt/profilesSignalProfileSummary[]
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}/revisionsSignalProfileRevision[]
GET /mqtt/revisions/{revisionId}SignalProfileRevision
GET /mqtt/profile-targetsDesiredSignalProfileActivation[]
GET /mqtt/displays/{id}/profileDesiredSignalProfileActivation
POST /mqtt/displays/{id}/profile{ profileRevisionId, expectedBindingEpoch, expectedActivationGeneration }DesiredSignalProfileActivation
POST /mqtt/displays/{id}/profile/retryDesiredSignalProfileActivation

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.

HTTPCodeCause
404signal_profile_not_found / signal_profile_revision_not_found / signal_profile_target_not_found / display_not_foundThe addressed object does not exist in this organization
409signal_profile_revision_conflictexpectedRevision does not match the current draft revision
409signal_profile_archivedDraft save or publish on an archived profile
409display_binding_epoch_conflictThe display was rebound; expectedBindingEpoch is stale
409signal_profile_activation_conflictexpectedActivationGeneration does not match the current generation
409signal_profile_capability_missingThe controller has not advertised the signal_profile capability
409signal_profile_capability_insufficientThe revision exceeds the advertised size, phase, white, or CCT limits
409signal_profile_target_healthyRetry requested on a target with no recorded error
503MQTT_DISPLAY_NOT_CONFIGUREDThe 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:

TopicDirectionDelivery
{base}/profile/configCloud → displayRetained, QoS 1. One complete profile envelope per activation.
{base}/credential/{credentialId}/profile/statusDisplay → cloudQoS 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.

FieldTypeMeaning
profile_sync_version1Envelope schema version
organization_idUUIDOwning organization
display_idUUIDTarget Status Display
binding_epochinteger ≥ 1Display Binding epoch
activation_generationinteger ≥ 1Monotonic generation within the binding
profile_revision_idUUIDDesired Signal Profile Revision
format_version1Compiled profile format version
byte_length29–750Exact decoded byte count
sha25664 lowercase hexSHA-256 of the canonical binary
created_atRFC3339 UTCEnvelope creation time
expires_atRFC3339 UTCTransport deadline; at most seven days after created_at
database64 ≤ 1000 charsThe 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:

FieldMeaning
activeThe actual active activation identity, or null when no valid profile is installed
attemptedThe candidate identity this result concerns; null for an unsolicited announcement
highest_admittedThe durable admission-fence identity; null only before the first admission in a new binding
resultapplied | rejected | deferred | announced
refresh_requestedTrue when the retained desired envelope needs a fresh transport deadline
error_codeOne 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

ValueDraft nameBehavior while the feed is stale
0per_pixelEach pixel renders its UNKNOWN binding
1all_pixelsEvery mapped pixel renders the stale program
2first_pixelLogical 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:

FieldMeaning
format_versionCompiled format the decoder supports (1)
max_decoded_bytesDecoder allocation bound (29–750)
max_phasesMaximum phases per pattern (0–8)
supports_white_outputWhite channel is wired and supported
supports_cctCCT 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.