Education

Q-SYS QRC — Remote Control Protocol

QRC (Q-SYS Remote Control) is the primary integration protocol for third-party control systems — Crestron, AMX, Savant, Lutron, and others — to read and write Q-SYS Named Controls in real time. It uses JSON-RPC 2.0 framing over a persistent TCP connection on port 1710. Any control system or custom application that can open a TCP socket and send JSON strings can integrate with a Q-SYS design via QRC. No Q-SYS plugin is required on the third-party system — QRC is a raw protocol documented publicly by QSC.

See control-systems/qsc-qsys-overview for the Q-SYS platform overview and control-systems/qsc-lua-scripting for scripting within Q-SYS itself.

Protocol Fundamentals

QRC runs over TCP, port 1710 (default, configurable in Q-SYS Administrator). The connection is persistent — open once at system startup, keep alive, reuse for all commands. Each QRC message is a UTF-8 JSON string terminated with a null byte (\0). Messages may be sent in either direction: the control system sends commands, and Q-SYS sends responses and unsolicited change notifications.

JSON-RPC 2.0 structure:

{
  "jsonrpc": "2.0",
  "method": "Control.Set",
  "params": {
    "Name": "MyGain",
    "Value": -10.0,
    "Ramp": 2.0
  },
  "id": 1234
}

The id field is an arbitrary integer chosen by the caller. Q-SYS echoes it in the response, allowing asynchronous command/response matching. Notifications (change group updates) use id: null.

Authentication (optional, enabled in Q-SYS Administrator):

{
  "jsonrpc": "2.0",
  "method": "Logon",
  "params": {
    "User": "admin",
    "Password": "password"
  },
  "id": 1
}

If authentication is not enabled on the Core, skip the Logon call — the connection is immediately ready for commands.

Named Controls

Named Controls are the integration surface of a Q-SYS design. In Q-SYS Designer, any control — a gain knob, a mute button, a router crosspoint, a UCI page selector — can be assigned a unique name string. That name is then addressable via QRC.

Creating a Named Control in Designer:

  • Right-click any control pin → "Add External Control Name"
  • Enter a descriptive string: "MainGain", "MicMute.1", "RoomMode"
  • Named Controls appear in the External Control panel (Tools → External Control)

Control types and their values:

Control TypeValue TypeRange / Options
Gain (dBFS)FloatTypically −100.0 to 0.0
Fader (0–100%)Float0.0 to 100.0
Mute / toggleFloat0.0 = off, 1.0 = on
String displayStringAny UTF-8 string
Selector / enumFloatInteger index (0-based)
Trigger / buttonFloat1.0 = press; auto-resets

Named Controls that are read-only (status indicators, meter levels) return values on Get but reject Set commands with an error.

Core QRC Methods

Control.Get

Read the current value of one or more Named Controls.

{
  "jsonrpc": "2.0",
  "method": "Control.Get",
  "params": ["MainGain", "MicMute.1", "RoomMode"],
  "id": 10
}

Response:

{
  "jsonrpc": "2.0",
  "result": [
    {"Name": "MainGain", "Value": -12.0, "String": "-12.0 dB", "Position": 0.73},
    {"Name": "MicMute.1", "Value": 0.0, "String": "unmuted", "Position": 0.0},
    {"Name": "RoomMode", "Value": 1.0, "String": "Presentation", "Position": 0.5}
  ],
  "id": 10
}

Each result includes Value (numeric), String (human-readable label from the control), and Position (0.0–1.0 normalized).

Control.Set

Write a value to a Named Control.

{
  "jsonrpc": "2.0",
  "method": "Control.Set",
  "params": {
    "Name": "MainGain",
    "Value": -18.0,
    "Ramp": 1.5
  },
  "id": 11
}

Ramp (optional) specifies a fade time in seconds. Omit for instant change. Setting a mute:

{"method": "Control.Set", "params": {"Name": "MicMute.1", "Value": 1.0}, "id": 12}

Control.SetString

Set a string-type Named Control (text displays, status fields):

{
  "jsonrpc": "2.0",
  "method": "Control.SetString",
  "params": {"Name": "StatusDisplay", "Value": "Meeting in progress"},
  "id": 13
}

Change Groups — Push Subscriptions

Polling Q-SYS with repeated Control.Get calls is functional but inefficient. The preferred pattern is Change Groups: register a set of Named Controls into a group, and Q-SYS pushes updates to the TCP connection whenever any control value changes.

Create a Change Group

{"jsonrpc":"2.0","method":"ChangeGroup.AddControl","params":{"Id":"cg1","Controls":["MainGain","MicMute.1","RoomMode"]},"id":20}

Set Poll Interval (fallback polling within the group)

{"jsonrpc":"2.0","method":"ChangeGroup.AutoPoll","params":{"Id":"cg1","Rate":0.1},"id":21}

Rate is in seconds. 0.1 = 100 ms poll rate as a backstop. Q-SYS still sends immediate notifications on change — the poll rate just ensures you don't miss slow-changing values.

Unsolicited Change Notification (from Q-SYS to control system):

{
  "jsonrpc": "2.0",
  "method": "ChangeGroup.Poll",
  "params": {
    "Id": "cg1",
    "Changes": [
      {"Name": "MicMute.1", "Value": 1.0, "String": "muted", "Position": 1.0}
    ]
  },
  "id": null
}

The control system should parse all incoming TCP data for id: null messages — these are change notifications, not responses to commands.

Destroy a Change Group

{"jsonrpc":"2.0","method":"ChangeGroup.Destroy","params":{"Id":"cg1"},"id":22}

Component Method Calls

Beyond Named Controls, QRC can call methods on Q-SYS components directly — useful for triggering snapshots, recalling presets, or controlling components without pre-defined Named Controls.

Get Component Controls

{"jsonrpc":"2.0","method":"Component.GetControls","params":{"Name":"MyRouter"},"id":30}

Set Component Control

{
  "jsonrpc":"2.0",
  "method":"Component.Set",
  "params":{
    "Name":"MyRouter",
    "Controls":[{"Name":"input.1.gain","Value":-6.0}]
  },
  "id":31
}

Trigger a Snapshot

{
  "jsonrpc":"2.0",
  "method":"Snapshot.Load",
  "params":{"Name":"MySnapshot","Bank":1},
  "id":32
}

Integration with Third-Party Control Systems

Crestron

Crestron SIMPL+ or SIMPL# modules open a TCP client socket to the Q-SYS Core IP on port 1710. A TCPClient SIMPL symbol handles the connection; string parsing extracts JSON responses. Several pre-built Crestron modules for Q-SYS QRC are available on the Crestron developer exchange. In SIMPL#, use Newtonsoft.Json or System.Text.Json for JSON parsing. Key consideration: Crestron's TCP buffer is limited — if Q-SYS sends many change notifications in rapid succession, buffer overflow can drop messages. Use a dedicated change group per functional area to limit notification volume.

AMX / Harman

AMX NetLinx uses IP_CLIENT_OPEN to establish the TCP connection and SEND_STRING / DATA_EVENT to exchange messages. NetLinx string handling is byte-level — JSON null terminators must be explicitly appended ("$00"). The AMX developer portal has community-contributed Q-SYS QRC NetLinx modules. Parsing JSON in NetLinx is manual (string search functions) since there is no native JSON library.

Savant

Savant uses its Blueprint programming environment and TCP component to communicate with Q-SYS via QRC. Named Controls map to Savant service requests. Savant's Q-SYS integration is well-documented in the Savant developer portal and commonly used in high-end residential/hospitality installs where Q-SYS handles audio and Savant handles the broader automation.

Custom Applications

Any language with TCP socket support works: Python (socket module), Node.js (net module), C#, etc. Python example:

import socket, json

sock = socket.socket()
sock.connect(('192.168.1.100', 1710))

cmd = {"jsonrpc":"2.0","method":"Control.Get","params":["MainGain"],"id":1}
sock.sendall((json.dumps(cmd) + '\x00').encode())

response = b""
while True:
    chunk = sock.recv(4096)
    response += chunk
    if b'\x00' in response:
        break

result = json.loads(response.rstrip(b'\x00'))
print(result)

Keepalive and Connection Management

Q-SYS closes idle QRC connections after approximately 30 seconds of inactivity. Two strategies prevent disconnection:

  1. ChangeGroup.AutoPoll — the polling mechanism generates traffic, keeping the connection alive as a side effect.
  2. No-op ping — send a StatusGet command every 15–20 seconds:
{"jsonrpc":"2.0","method":"StatusGet","id":99}

This returns Core status (firmware version, design name, status) and resets the idle timer.

Control systems should implement reconnection logic: if the TCP connection drops (Core reboot, network interruption), re-establish the connection and re-subscribe to all change groups. Change group subscriptions are not persistent across connections.

Common Pitfalls

  • Null terminator missing. QRC messages must be terminated with a \0 byte (ASCII 0x00), not a newline. Sending \n instead of \0 causes Q-SYS to buffer the message indefinitely waiting for the terminator — no response is ever returned. The control system appears to work but hangs. Always append \x00 after the JSON string.

  • Named Control not exposed. A control that exists in Q-SYS Designer but was not given an External Control Name is invisible to QRC — Control.Get returns an error "Control not found". This is the most common integration gotcha: the programmer assumes all controls are accessible. Only controls explicitly named in Designer's External Control panel are reachable via QRC.

  • Change group subscriptions lost after Core reboot. Change groups exist only in Core RAM — they are not stored in the design. If the Core reboots or loses power, all subscriptions are gone. The control system must detect the dropped TCP connection, reconnect, and re-register all change groups. Without reconnection logic, the control system silently stops receiving updates after any Core restart.

  • Polling rate overwhelming control system buffer. Setting ChangeGroup.AutoPoll rate too low (e.g., 0.01 seconds = 10 ms) on a change group with many controls generates extremely high notification volume. A Crestron or AMX processor with limited TCP receive buffer will drop messages. Use 0.1–0.5 s poll rates; rely on immediate push notifications for time-sensitive controls.

  • Authentication mismatch after Core reset. If QRC authentication is enabled in Q-SYS Administrator and the password is changed, existing integrations stop working with a generic connection error. Document the QRC password in the system password log and use a dedicated integration credential separate from the admin password.

  • Component.Set vs Control.Set confusion. Control.Set targets Named Controls (the External Control list). Component.Set targets internal component controls by component name and control name within the design. Using Component.Set on a component that was renamed in Designer breaks the integration silently — Named Controls are more stable because they are explicitly maintained by the programmer.

We use optional analytics cookies to understand site usage and improve the experience. You can accept or reject.