MQTT Interface
Documentation

Rotoclear exposes 67 canonical commands for imaging, recording, streaming, AI and GPIO. All commands support correlated JSON requests through execute_v1; eight also provide a direct topic form. set_parameter contains 42 setting keys and configure_backup contains three policy keys. These are parameters, not additional command names.

  • Http API
  • WebSocket API
  • Camera stream
  • State Description
  • AI module
  • GPIO module

Connection, topics and request contract

Use the device's configured embedded broker or external broker. The device MQTT client must be running and connected to that broker. Embedded credentials/ACLs are synchronized from eligible device users; external brokers must enforce the same per-user topic ownership. Configure broker listeners, TLS/trust and client settings through the device management interface.

The embedded internal listener uses 127.0.0.1:1883. This loopback listener is not a LAN endpoint. Use the configured external listener (TLS port 8883 by default) for remote clients.

The embedded broker uses the device’s HTTPS certificate and private key by default and follows renewal of device-managed HTTPS material. An uploaded custom MQTT server certificate is preserved until you explicitly reset it to the HTTPS certificate. Connect using a hostname or IP in the certificate’s Subject Alternative Name (SAN), and configure clients with the appropriate trusted issuer chain. /media/data/mqtt/ca.crt can contain a certificate rather than a complete issuing-CA chain; do not assume that exporting this file alone establishes normal CA-chain trust. Keep hostname and certificate verification enabled. See Certificates and TLS for provisioning.

Enable the MQTT module in Settings → Modules and configure its MQTT section on that same page. An active mqtt license is required to run the service and execute commands; configuration can still be read and saved while inactive. Choose the embedded or external broker, credentials, topic prefix and device ID, save, then start the device client. Embedded mode manages the local broker as part of startup. Confirm that the client reports connected. Configure application users and their API permissions before connecting an integration client.

Purpose Topic when device IDs are enabled
Versioned request <prefix>/<device_id>/cmd/users/<username>/execute_v1
Correlated response <prefix>/<device_id>/response/users/<username>/execute_v1
Direct request/response Same paths, replacing execute_v1 with one of the eight retained direct commands
Per-user capability discovery <prefix>/<device_id>/response/users/<username>/capabilities
Private automation state <prefix>/<device_id>/response/users/<username>/automation_state
Existing shared events <prefix>/<device_id>/events/#
Existing device status <prefix>/<device_id>/camera/status

Omit / when configured with include_device_id=false. Subscribe to replies before publishing. Always publish requests non-retained. All 67 operations accept a versioned request through execute_v1; only the eight commands below also have direct-topic handlers. Each is one operation with two transport forms, not two command names.

Replace the illustrative expiry with device-compatible Unix milliseconds strictly in the future and at most 60 seconds ahead. Calculate expiry from the current time when sending each new request; keep client and device clocks synchronized. Give every intentional action a unique request ID. Request IDs are 1–64 bytes; requests are limited to 4096 bytes. Required fields are version, request_id, command, expires_at_ms; params defaults to {} where allowed. Unknown fields/types/commands are rejected. User identity comes from the ACL-protected topic, never a payload owner field.

"version": 1,
  "request_id": "image-001",
  "command": "set_parameter",
  "params": {
    "name": "brightness",
    "value": 2
  },
  "expires_at_ms": 1791806400000
}

Responses contain version, request_id, command, timestamp_ms, phase, success, data, and error fields when applicable. Phases are rejected, failed, accepted, applied, and completed. applied normally confirms API configuration readback; accepted reports asynchronous intent, not physical completion. GPIO direct output additionally confirms driver readback. Read-only queries use completed.

Structured mutating requests use duplicate protection scoped to the topic user. Identical ID, command, parameters and expiry replay the cached result while the request remains valid; conflicting mutation IDs are rejected. Entries expire at expires_at_ms, with a five-minute monotonic upper bound, so the normal retry window is at most 60 seconds. get_state and status always read fresh state and do not use the cache. Replies are non-retained QoS 1 and limited to 16 KiB. Asynchronous actions report progress through state queries rather than later correlated completion callbacks.

Certificates and TLS

Use the certificate controls in Settings → Modules → MQTT. Server identity, trust for incoming client certificates and trust for an external broker are separate settings. Certificate uploads and removals automatically reload or restart the affected connection; a separate reload button is not required. After a change, reconnect integration clients and check the device connection status. A successful upload reports installation, not a completed TLS handshake.

Purpose Provisioning
Embedded server certificate Upload a PEM server certificate/chain and matching private key. Put the leaf certificate first, followed by issuing intermediates. An optional passphrase unlocks an encrypted uploaded key. Reset selects the device HTTPS certificate again.
Incoming client certificates (mutual TLS) Upload the trusted client CA or CA bundle, then enable tls.require_client_cert. The certificate display lists every CA in the bundle. The switch only enforces client certificates when a client CA file exists; removing that CA removes this enforcement. Application username/password and topic permissions still apply.
External broker Upload its trusted CA bundle. If the broker requires device client authentication, also upload a matching client certificate/key pair and enable tls.require_client_cert. Configure the external endpoint, TLS and broker credentials separately.

Uploads check PEM readability and certificate/key matching. They do not establish that an external client trusts the chain or that the configured hostname matches its SAN. Use a correctly issued chain for normal CA validation, or an explicitly supported certificate-pinning configuration. The device skips TLS for broker hosts literally named localhost or 127.0.0.1, including external mode; use a certificate-matching named endpoint when TLS is required.

For automation, send authenticated POST /api/mqtt/certificates?kind= with multipart/form-data and NetworkSettings_rw permission. The whole request body is limited to 256 KiB. PEM fields are certificate, key, ca, and optional passphrase. Transfer private keys only over HTTPS.

kind Fields / effect
server Required certificate and key; optional passphrase. Installs a custom embedded server identity.
server_reset No PEM fields. Restores the device HTTPS identity.
client_ca Required ca. Installs/replaces the incoming client trust bundle.
client_ca_remove No PEM fields. Removes incoming client trust.
external Required ca; optional certificate and key together, plus passphrase. A CA-only upload does not remove an already installed client pair.
external_remove No PEM fields. Removes external CA and device client credentials.

Success returns HTTP 200 with {"ok":true}; installation/validation errors return HTTP 400 with {"ok":false,"error":"…"}. Missing permission returns 403 and oversized uploads return 413. Inspect mqtt_certificates for certificate source, identity and CA-bundle details.

Configuration, sessions and recovery

Read mqtt_config, write configuration with NetworkSettings_rw, and inspect mqtt_config_validation_result before starting. Include mode (embedded or external) in configuration writes. Saved configuration lives in /media/data/config/mqtt.json. The management HTTP endpoints are GET /api/mqtt/config, POST /api/mqtt/config and GET /api/mqtt/status.

Use mqtt_action with an action value of start_client, stop_client, restart_client, restart_broker, reload_acl or reload_certificates. The HTTP POST /api/mqtt/actions interface accepts the four restart/reload actions, not start/stop. Starting applies saved configuration; observe client/broker state after asynchronous transitions. Module activation, including activation during application startup, starts MQTT from saved settings. The exposed startup.auto_start field is not used to gate this activation path; do not rely on it to keep an active module stopped after reboot.

connection.session_persistent defaults to false (clean session); existing saved values are preserved. Enabling it lets the broker retain subscriptions and queue eligible messages during disconnection. Queued direct commands have no expiry and can execute after reconnection. Prefer expiring execute_v1 requests for controls. This setting is distinct from the application result cache and from durable broker storage. The outgoing in-memory queue is bounded by connection.publish_queue_limit and drops its oldest item when full.

For an external broker, the application derives the caller from the topic username; it cannot inspect the publishing client’s authenticated broker identity. Configure broker ACLs so each client can publish only its own command topics and read only its own private responses. Only the device service identity may publish responses/capabilities. TLS alone does not enforce these ownership rules.

Direct-topic commands

These names are also valid inside an execute_v1 envelope. New clients should use the envelope for client-chosen IDs, expiry and duplicate protection.

Commands Direct payload Envelope params and behavior
take_picture Ignored {}; asynchronous capture through the shared API
video_start Empty or {} {}; normal recording only
video_stop Ignored {}; generic stop for the current recording/pending start
set_rpm0, set_rpm1 Raw JSON integer {"value":1500}; target RPM for the selected head, not measured RPM
rtsp_on, rtsp_off Ignored {}; RTSP service intent
status Ignored {}; derived summary with MQTT uptime, device ID, software version and recording intent

RPM values must be nonnegative signed-32-bit integers. This is a representation bound, not a hardware-certified speed range. The existing API motor behavior remains in effect. Direct deliveries get generated IDs and have no duplicate protection. status replies on the user's response topic and, for direct requests, preserves a limited shared events/status snapshot. It is distinct from get_state, which reads selected raw API fields.

Retired duplicate names and migration

Breaking change: the following 23 names no longer execute, on either direct topics or execute_v1. They return COMMAND_REPLACED with a replacement description and are absent from capability discovery. Publish replacement requests through execute_v1; do not merely rename the old direct topic. Replies then arrive on the execute_v1 response topic and use the result envelope's data object.

Retired names Canonical replacement Reason
light_0_on, light_0_off, light_1_on, light_1_off light0_on, light0_off, light1_on, light1_off Four identical boolean writes
camera_head_0, camera_head_1 select_head0, select_head1 Identical head-selection writes
stream_freeze, stream_unfreeze freeze_stream_on, freeze_stream_off Identical freeze writes
backup_start, backup_stop visual_backup_on, visual_backup_off Identical backup-intent writes
set_light_0_brightness, set_light_1_brightness set_parameter, name light0Brightness / light1Brightness, with value Already covered by the typed allowlist
set_focus_cam0_sensor0, set_focus_cam0_sensor1, set_focus_cam1_sensor0, set_focus_cam1_sensor1 set_parameter, matching focusCam0Sensor0 / focusCam0Sensor1 / focusCam1Sensor0 / focusCam1Sensor1, with value Already covered by the typed allowlist
select_sensor_cam0, select_sensor_cam1 head0_sensor0 / head0_sensor1, or head1_sensor0 / head1_sensor1 Choose the fixed command matching the former index
camera_settings get_state with the seven image keys listed below Same readable values, explicit key selection
stream get_state with the six stream keys listed below Same readable values, explicit key selection
system get_state with systemDescription, systemPerformance Both fields are now in MQTT's read allowlist
autofocus_center set_parameter, name:"tapToFocusView", value:{"x":0.5,"y":0.5} Preset of an existing typed control
zoom_reset set_parameter, name:"zoomState", value:{"zoom":1} Preset of an existing typed control

Former camera_settings keys: exposure, brightness, contrast, whiteBalance, noiseSuppression, sharpness, saturation.

Former stream keys: resolution, fps, streamingResolution, streamingFps, streamingBitrate, freeze.

For system, former response labels description and performance become raw state keys systemDescription and systemPerformance in data. There is no separate grouped-query handler or duplicated top-level result data.

The optional video_start.mode input was also removed. Use video_start, timelapse_start, or slowmotion_start, each with {}. Even mode:"normal" is rejected; empty parameters give one explicit command per recording mode. slowmo is not a separate alias.

video_stop, timelapse_stop, and slowmotion_stop remain distinct: the latter two require the matching active mode, while video_stop is generic. Opposite on/off actions and different head/sensor targets remain separate because they request different state changes. Shared status-event topics remain available; removing grouped query commands does not remove those event topics.

Fixed operational commands

Head 0 and head 1 commands address the numbered camera, light or disc independently. select_head0/1 selects the active view; headN_sensorM selects that head’s sensor. Layout commands select single view, the named picture-in-picture corner, or side-by-side. Automatic exposure, white balance and noise suppression toggle the corresponding image setting; auto-rotate and disc controls address the named head. Stream/record time/date commands toggle those overlays. Audio commands toggle recording or backup audio, subject to configured audio input. visual_backup_on/off sets backup intent for the configured backup storage.

These names use empty {} parameters. Three also appear in the direct-topic table above and are counted once. All others require execute_v1.

Area Commands
Capture/recording take_picture, video_start, video_stop, timelapse_start, timelapse_stop, slowmotion_start, slowmotion_stop
Lights/freeze light0_on, light0_off, light1_on, light1_off, freeze_stream_on, freeze_stream_off
Head/sensor select_head0, select_head1, head0_sensor0, head0_sensor1, head1_sensor0, head1_sensor1
Layout layout_single, layout_pip_bottom_left, layout_pip_top_left, layout_pip_top_right, layout_pip_bottom_right, layout_side_by_side
Automatic image modes auto_exposure_on, auto_exposure_off, auto_white_balance_on, auto_white_balance_off, noise_suppression_on, noise_suppression_off
Orientation head0_auto_rotate_on, head0_auto_rotate_off, head1_auto_rotate_on, head1_auto_rotate_off
Disc motors head0_disc_on, head0_disc_off, head1_disc_on, head1_disc_off
Backup visual_backup_on, visual_backup_off
Overlays stream_time_on, stream_time_off, stream_date_on, stream_date_off, record_time_on, record_time_off, record_date_on, record_date_off
Audio record_audio_on, record_audio_off, backup_audio_on, backup_audio_off

Head/sensor/focus/layout availability is checked. Shared API recording services enforce camera/storage and resource restrictions. timelapse_stop and slowmotion_stop require the matching active mode. MQTT video_stop remains a generic stop for legacy compatibility.

Command Parameters Purpose
set_parameter {"name":"brightness","value":2} One of the 42 validated settings below
configure_backup {"name":"backupNumberOfSavedHours","value":24} Separate storage-policy operation
get_state {"keys":["kiControl","gpioInput","gpioOutput"]} 1–16 allowlisted state keys; read permission required for every key
Group Accepted keys
Image brightness, saturation, expo, gain, contrast, sharpness, redGain, blueGain
Illumination/focus light0Brightness, light1Brightness, focusCam0Sensor0, focusCam0Sensor1, focusCam1Sensor0, focusCam1Sensor1
Resolution/FPS fotoResolution, resolution, streamingResolution, fps, streamingFps
Quality/codec pictureQuality, streamingQuality, recordingQuality, slowmoQuality, streamingFormat, recordingFormat
Special recording rates highSpeedMode, timelapseInputFps, timelapseOutputFps
Framing tapToFocusView, zoomState
Bitrate streamingBitrate, streamingMaxBitrate, recordingMaxBitrate
Overlay style streamingTimestampSetting, recordingTimestampSetting
Naming changeVideoName, changeImageName, changePostName, changeBackupPostName
Orientation/pre-roll alignmentStream0, alignmentStream1, prerollSettings

Typed parameters

set_parameter requires exactly params.name and params.value. Only these names are exposed:

Names JSON type and accepted values
streamingBitrate Integer 0…100,000,000 bits/s; 0 restores quality-based selection; effective encoder output is still limited by the configured cap
streamingMaxBitrate, recordingMaxBitrate Integer 10,000,000…100,000,000 bits/s
streamingTimestampSetting, recordingTimestampSetting Nonempty object patch: x, y numbers 0…1; scaleValue number 0.4…2.2; drawBackdrop boolean; backdropColor, textColor integers 0…4,294,967,295 in the API's packed color format
prerollSettings Nonempty object patch: enabled boolean and/or durationSec integer 15…30
changeVideoName, changeImageName String filename prefix, 0…17 bytes; reserved value Backup rejected
changePostName, changeBackupPostName String filename suffix, 0…55 bytes
alignmentStream0, alignmentStream1 String "0" for normal orientation or "1" for the alternate hardware orientation; corresponding head must exist; not an angle in degrees
brightness, saturation Integer −7…7
expo Number 0.01…1000, in existing API exposure units
gain Integer 0…42
contrast Number 1…2.5
sharpness Integer 0…15
redGain, blueGain Integer −128…127
light0Brightness, light1Brightness Integer 0…100; connected, dimmable head required
focusCam0Sensor0, focusCam0Sensor1, focusCam1Sensor0, focusCam1Sensor1 Integer 0…1023; corresponding focus motor required
pictureQuality Integer 10…90
streamingQuality, recordingQuality, slowmoQuality Integer 1…100
fotoResolution, resolution, streamingResolution String HD, FullHD, 4K
fps, streamingFps Integer 1, 5, 10, 15, 24, 30, 60
highSpeedMode Integer 120, 240, 480, 600, 720, 960
timelapseInputFps Number 0.01…1
timelapseOutputFps Number 1…60
streamingFormat String h264, hevc
recordingFormat String h264, hevc, mjpg
tapToFocusView Object with both x and y, numbers 0…1 in the current view
zoomState Nonempty object: zoom number 1…4, or up to 7 for a head with tele-sensor focus support; pan uses both left and top, numbers 0…1

For example:

Recording/backup setting restrictions from the API still apply. A rejected setter or readback mismatch returns failed. Zoom and autofocus return accepted because hardware motion is asynchronous. These controls do not introduce new guarantees beyond the API's hardware behavior.

"version": 1,
  "request_id": "plc-004",
  "command": "set_parameter",
  "params": {
    "name": "tapToFocusView",
    "value": {
      "x": 0.4,
      "y": 0.6
    }
  },
  "expires_at_ms": 1791806400000
}

Examples for structured settings

For overlays and pre-roll, omitted fields retain their previous values. Unknown fields, empty objects and string-encoded objects are rejected. The result's data.value contains the full accepted object; numeric integer/float representation differences do not cause false failures. prerollSettings describes configuration; read prerollStatus to check active buffering, duration and capacity. Special recording modes can suspend buffering even when configured enabled.

Filename components accept ASCII letters, digits, hyphen, underscore and dot. Empty strings clear the component and restore normal API naming behavior. Spaces, non-ASCII text, directory separators, NUL, . and .. are rejected before invoking the API, so names are not silently sanitized or truncated. Naming changes affect subsequent captures/files, not names of existing recordings. The 17/55-byte limits match the current API setter's inclusive substring limits.

Bitrate caps use the existing API's 10–100 Mbit/s bounds. The target bitrate's 100 Mbit/s ceiling is an command contract limit; it is not a claim that all codecs or modes can achieve it. Existing recording resolution/FPS changes may recompute the recording cap. Query accepted settings after such changes.

"version": 1,
  "request_id": "overlay-001",
  "command": "set_parameter",
  "params": {
    "name": "streamingTimestampSetting",
    "value": {
      "x": 0.05,
      "y": 0.9,
      "scaleValue": 1.2,
      "drawBackdrop": true
    }
  },
  "expires_at_ms": 1791806400000
}
{
  "version": 1,
  "request_id": "preroll-001",
  "command": "set_parameter",
  "params": {
    "name": "prerollSettings",
    "value": {
      "enabled": true,
      "durationSec": 30
    }
  },
  "expires_at_ms": 1791806400000
}
{
  "version": 1,
  "request_id": "name-001",
  "command": "set_parameter",
  "params": {
    "name": "changePostName",
    "value": "Part_123"
  },
  "expires_at_ms": 1791806400000
}
{
  "version": 1,
  "request_id": "bitrate-001",
  "command": "set_parameter",
  "params": {
    "name": "streamingMaxBitrate",
    "value": 20000000
  },
  "expires_at_ms": 1791806400000
}

Backup retention policy

configure_backup invokes only the following three API setters with storage-policy permission; it cannot change storage credentials or select a storage device.

params.name params.value
backupUseCompleteSpace Boolean; choose use-all-space versus configured retention capacity
backupNumberOfSavedHours Integer 1…8760 hours; backup storage must be available
backupOnReachingRecordingTimeCapacityAction String stopRecording or reduceMaxTime

The 8760-hour ceiling is a deliberate command limit, not a storage-capacity guarantee. The API may reduce the requested hours to fit available storage. A successful retention result includes requested, value, configured_hours and adjusted. For example, requested:24, value:12, adjusted:true means the API selected 12 hours. The exposed write key is backupNumberOfSavedHours; backupNumberOfSavedHoursSet is read-only and reports effective configuration.

These settings use existing backup behavior, including capacity notifications, stopping/reducing retention at capacity, and automatic cleanup of eligible backup media. Changes apply as individual operations, not as an atomic multi-setting transaction. Read the policy settings and remaining capacity after configuring them.

"version": 1,
  "request_id": "retention-001",
  "command": "configure_backup",
  "params": {
    "name": "backupNumberOfSavedHours",
    "value": 24
  },
  "expires_at_ms": 1791806400000
}

AI commands

Command Parameters Required API write permission
ai_control Nonempty patch of active boolean, project enum, confidence number 0–1 RecordingSettings_rw or Recording_rw
ai_model project, model string up to 128 bytes; empty model clears selection RecordingSettings_rw
ai_runtime project, nonempty settings object RecordingSettings_rw or SystemSettings_rw

Projects: humandetect, spandetekt, motiondetect. Starting AI requires an active license, connected camera, available processing resources and a selected installed model where the project requires one. motiondetect does not use a selectable model. Stop remains possible when the license is inactive. Stop AI and wait for process exit before changing project/confidence; an active project's model cannot be changed.

Runtime settings accept displayMode (all, or relevant for spandetekt), showTool, profile, autoTag, deletable, window (0.3–60), integer triggerTime/gap/cooldown (0–3600), classes, and ampel. Profiles must be advertised by the selected model. Class maps allow at most 16 entries; class keys have at most 64 bytes. Numeric class/traffic-light thresholds are 0–10000; positive warn must not exceed positive crit. ampel requires window and classes. These are transport bounds and count-based thresholds, not necessarily confidence probabilities.

Omitted top-level settings are preserved. Supplied nested class/traffic-light objects replace those nested values. Null reset, arbitrary runtime keys, model upload/delete and producer ingestion are not exposed. AI start/stop results report accepted; use kiControl, kiProjects, and processAmpel for actual process/decision state. Stored runtime settings still require producer support. These services are shared with OPC but execute with the MQTT caller's permissions.

"version": 1,
  "request_id": "ai-start-001",
  "command": "ai_control",
  "params": {
    "project": "motiondetect",
    "confidence": 0.35,
    "active": true
  },
  "expires_at_ms": 1791806400000
}
{
  "version": 1,
  "request_id": "ai-model-001",
  "command": "ai_model",
  "params": {
    "project": "humandetect",
    "model": "installed-model.hef"
  },
  "expires_at_ms": 1791806400000
}
Runtime settings accept displayMode (all, or relevant for spandetekt), showTool, profile, autoTag, deletable, window (0.3–60), integer triggerTime/gap/cooldown (0–3600), classes, and ampel. Profiles must be advertised by the selected model. Class maps allow at most 16 entries; class keys have at most 64 bytes. Numeric class/traffic-light thresholds are 0–10000; positive warn must not exceed positive crit. ampel requires window and classes. These are transport bounds and count-based thresholds, not necessarily confidence probabilities.

{
  "version": 1,
  "request_id": "ai-runtime-001",
  "command": "ai_runtime",
  "params": {
    "project": "spandetekt",
    "settings": {
      "autoTag": true,
      "window": 2,
      "ampel": {
        "window": 2,
        "classes": {
          "0": {
            "warn": 0.25,
            "crit": 0.5
          }
        }
      }
    }
  },
  "expires_at_ms": 1791806400000
}

GPIO commands

All three require StreamSettings_rw and enabled/licensed GPIO. Command and returned single-binding pin numbers are 1–4; raw binding arrays from get_state retain API indexes 0–3. Input/output state objects use pin1…pin4 string levels "0"/"1".

Command Parameters Behavior
gpio_output pin, boolean value Checked driver write and readback; rejects an output controlled by a binding or any active legacy output task
gpio_input_binding pin, action Persistent input-edge action: light0, light1, or recording; caller must also have the target action's write permission
gpio_output_binding pin, source, source-specific fields; optional active_high boolean (default true) Persistent source-driven output; caller must be allowed to read that source
Either binding command Positive id, delete:true only Delete an existing binding owned by this MQTT user
Output source Additional fields
vbActive, light0, light1, internet, reconstruction3d None
recording when: start or stop
kiAmpel instance: supported AI project; when: green, orange, red, or gray
"version": 1,
  "request_id": "gpio-001",
  "command": "gpio_output",
  "params": {
    "pin": 1,
    "value": true
  },
  "expires_at_ms": 1791806400000
}
{
  "version": 1,
  "request_id": "gpio-input-001",
  "command": "gpio_input_binding",
  "params": {
    "pin": 1,
    "action": "recording"
  },
  "expires_at_ms": 1791806400000
}
{
  "version": 1,
  "request_id": "gpio-ai-001",
  "command": "gpio_output_binding",
  "params": {
    "pin": 2,
    "source": "kiAmpel",
    "instance": "spandetekt",
    "when": "red",
    "active_high": true
  },
  "expires_at_ms": 1791806400000
}

The supported MQTT input actions remain light0, light1 and recording. The wider API gpioActions/gpioSources catalogs may contain additional entries; use this protocol’s command tables to determine accepted values. For example, freeze is not an accepted input-binding action.

For supported input actions, HIGH selects on/start and LOW selects off/stop. The current level is applied once after binding creation and GPIO initialization, then after subsequent level changes, following a 300 ms settling interval. A failed action or a later UI change is not continuously retried at the same level; another input transition is required. Duplicate pin/action combinations are rejected; input bindings are capped at 32 total and eight per MQTT user. One output binding occupies each pin. MQTT can replace its own output binding, but cannot replace or delete another user’s or an OPC/API-owned binding. Driver readback does not verify external wiring; binding creation confirms stored configuration.

MQTT binding ownership is server-assigned deviceRole:"mqtt_user" plus createdByUser:, persisted across restart. Input actions recheck GPIO and target-action permissions; output evaluations recheck GPIO and source-read permissions. Deleting a user removes that user’s MQTT input and output bindings, so recreating the same username does not inherit them. Permission revocation prevents further authorized actions/evaluations but does not automatically reset an output. Removing a binding leaves its last output level unchanged. No missing owner falls back to admin for MQTT bindings; naming a user opc-pubsub does not grant the OPC role.

Bindings keep running independently of broker connectivity while their owner's permissions allow it. The existing privileged API/OPC output-binding administration can still replace or remove output bindings. Output source inversion can make a missing/inactive source HIGH. Backup activity is requested intent; reconstruction state does not start reconstruction. Direct output levels are not persisted across reboot. General network tasks, arbitrary state writes, pulses and schedules remain outside this command interface.

AI/GPIO state and telemetry

Device availability uses separate /camera/status/online and /camera/status/lwt topics. Connection publishes retained online:true on the online topic; the Last Will publishes online:false on the LWT topic after unexpected disconnection. Do not treat a retained online message alone as proof of a current connection, or the Last Will timestamp as the exact disconnection time. Grouped state events under /events/# are retained QoS 1 snapshots. The /events/status/picture event (payload field picture_taken) is non-retained; old retained photo events are cleared when the device connects. Private request results are non-retained.

get_state includes kiControl, kiModels, kiRuntimeSettings, kiProjects, processAmpel, processGraph, reconstruction3dActive, gpioInput, gpioOutput, gpioInputBindings, gpioOutputBindings, gpioSources, and gpioActions, alongside the operational allowlist. Each read is permission-checked. Producer inputs kiSample/kiEvent remain unexposed.

The private automation_state topic publishes version, timestamp_ms, a state object and omitted_keys. It includes readable values from kiControl, kiProjects, processAmpel, gpioInput, and gpioOutput; it is sent on connect and on changes, coalesced to at most four snapshots per second. It uses non-retained QoS 1 and rechecks each user's read permissions on every send. Large fields are omitted to preserve result-size bounds; query individual fields instead. Existing shared event topics are not expanded to broadcast private AI/GPIO state. Snapshots are not correlated completion events.

State queries

get_state requires params.keys, an array of 1–16 allowed strings. Duplicate keys are returned once. Reads return current API state, not a hardware-synchronous snapshot. Every requested key requires read permission. MQTT also supports systemDescription and systemPerformance. The shared readable keys are:

cameraSwitch, cam0Exists, cam1Exists, cam0SelectedSensor, cam1SelectedSensor, headIsSlowMoCompatible, light0, light1, light0Brightness, light1Brightness, light0Dim, light1Dim, dictRotation0, dictRotation1, RPM0, RPM1, exposure, whiteBalance, noiseSuppression, brightness, expo, gain, contrast, sharpness, saturation, redGain, blueGain, pip, autoRotate0, autoRotate1, freeze, zoomState, focusView, focusCam0Sensor0, focusCam0Sensor1, focusCam1Sensor0, focusCam1Sensor1, takePicture, record, recordingMode, recordTime, recordingFilename, fotoFilename, backupActive, backupFilename, backupRemainingCapacitySeconds, fotoResolution, resolution, fps, streamingResolution, streamingFps, pictureQuality, streamingQuality, recordingQuality, slowmoQuality, streamingFormat, recordingFormat, timelapseInputFps, timelapseOutputFps, highSpeedMode, streamingWithTimeStamp, streamingWithDateStamp, recordingWithTimeStamp, recordingWithDateStamp, recordingAudioActive, backupAudioActive, streamingIsBusy, streamingBitrate, streamingMaxBitrate, recordingMaxBitrate, streamingTimestampSetting, recordingTimestampSetting, prerollSettings, prerollStatus, changeVideoName, changeImageName, changePostName, changeBackupPostName, alignmentStream0, alignmentStream1, backupUseCompleteSpace, backupNumberOfSavedHours, backupNumberOfSavedHoursSet, backupOnReachingRecordingTimeCapacityAction, kiControl, kiModels, kiRuntimeSettings, kiProjects, processAmpel, processGraph, reconstruction3dActive, gpioInput, gpioOutput, gpioInputBindings, gpioOutputBindings, gpioSources, gpioActions, fpgaTemp, oct0, oct1, psi0, psi1, swVersion, fmVersion.

Permissions and capability discovery

The broker authenticates the client and enforces topic ownership; the application separately checks the corresponding API permission for each command. Users must only publish to their own cmd/users//+ topics and subscribe to their own response/users//#. External broker administrators must enforce this separation, including protection against publishing forged responses. The device’s internal client needs access to incoming user commands and outgoing replies/telemetry.

Subscribe to your retained per-user capabilities topic for permitted commands, resolved reply paths, typed settings and envelope limits. Capabilities refresh on connection and permission changes; deleting a user clears its retained capabilities. execute_v1 describes the dispatcher and is not an extra permitted operation. Availability still depends on connected heads, storage, licenses and configuration. Every write and requested state key is permission-checked, including cached retries. Values requiring read permission are omitted when the caller lacks it.

Results, retries and errors

Phase Meaning
rejected Validation, authorization, expiry, retention or queue-capacity checks prevented execution
failed Execution failed or the accepted state differs from the request; read affected state before retrying
accepted Asynchronous action accepted; observe state for progress
applied Configuration readback matches the requested setting, or adjusted backup retention is reported
completed State query returned its data
"version": 1,
  "request_id": "image-001",
  "command": "set_parameter",
  "timestamp_ms": 1791806395000,
  "phase": "applied",
  "success": true,
  "data": {
    "name": "brightness",
    "requested": 2,
    "value": 2
  }
}
Error code Resolution
INVALID_ARGUMENT, INVALID_TOPIC Correct the JSON types, fields, ranges or user topic
UNKNOWN_COMMAND, COMMAND_REPLACED Use the canonical command table and migration mapping
EXPIRED Synchronize clocks; create a new intentional request with a fresh expiry
RETAINED_COMMAND Remove retained command messages and publish without retention
PERMISSION_DENIED Grant the relevant application permission and broker ACL
UNAVAILABLE Check camera, storage, module and resource requirements
REQUEST_ID_CONFLICT An ID already identifies different content; use a unique ID for new work
QUEUE_FULL The mutation cache reached 64 entries for this user or 1024 globally; wait for request expiry before retrying
NOT_APPLIED, EXECUTION_ERROR Inspect error_message and read current state before another action
RESULT_TOO_LARGE Request fewer or smaller state fields

The mutation cache holds at most 64 entries per user and 1024 globally. When either limit is reached, new structured mutations return QUEUE_FULL until entries expire; read-only queries do not consume slots. Cache state survives broker reconnects in the same application process, but not application restart. Retry a missing mutation reply with the identical envelope, including expiry, only while that expiry is valid. After expiry, query current state before deciding whether a new action is needed. Direct commands have neither expiry nor duplicate protection. The receive channel holds 100 events; overflow and requests over 4096 bytes can be dropped without a correlated reply. Receive-queue drops increment mqtt_status_update.statistics.commands_dropped. A broker delivery acknowledgement does not confirm execution.

For capture, compare takePicture and fotoFilename with their previous values. For recording, observe record, recordingMode, recordingFilename and recordTime. For AI, observe kiControl, kiProjects and processAmpel. A filename alone does not establish that writing has finished. Other clients can change the same state.

Client example

Install the Python paho-mqtt client in your integration environment. Set MQTT_HOST, MQTT_USER, MQTT_PASSWORD, MQTT_CA, and MQTT_BASE for your configuration. This example uses TLS, subscribes before publishing, and makes a read-only state request. The example requires Paho MQTT 2.x.

import paho.mqtt.client as mqtt

base = os.environ["MQTT_BASE"]  # prefix/device_id, or prefix only
user = os.environ["MQTT_USER"]
request_topic = f"{base}/cmd/users/{user}/execute_v1"
reply_topic = f"{base}/response/users/{user}/execute_v1"
request_id = str(uuid.uuid4())
done = threading.Event()

def on_connect(client, userdata, flags, reason_code, properties):
    if reason_code.is_failure:
        print("Connection rejected:", reason_code)
        done.set()
        return
    client.subscribe(reply_topic, qos=1)

def on_subscribe(client, userdata, mid, reason_codes, properties):
    if any(code.is_failure for code in reason_codes):
        print("Subscription rejected")
        done.set()
        return
    request = {"version": 1, "request_id": request_id,
               "command": "get_state", "params": {"keys": ["cam0Exists", "cam1Exists"]},
               "expires_at_ms": int(time.time() * 1000) + 30000}
    client.publish(request_topic, json.dumps(request), qos=1, retain=False)

def on_message(client, userdata, message):
    result = json.loads(message.payload)
    if result.get("request_id") == request_id:
        print(json.dumps(result, indent=2))
        done.set()

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
client.username_pw_set(user, os.environ["MQTT_PASSWORD"])
client.tls_set(ca_certs=os.environ["MQTT_CA"])
client.on_connect = on_connect
client.on_subscribe = on_subscribe
client.on_message = on_message
client.connect(os.environ["MQTT_HOST"], int(os.getenv("MQTT_PORT", "8883")))
client.loop_start()
if not done.wait(35):
    print("No correlated result received; check connection and device state")
client.disconnect()
client.loop_stop()