Control4 Subsystem Reference

# Voice Input Proxy

The contract between the Voice Coordinator and voice input devices: the push-to-talk remotes, touchscreens, and apps that carry the microphone. It covers the commands the coordinator pushes, the proxy variables the device reports, and how a microphone press finds its way to a voice target.

- **Proxy**: voiceinput.c4i

- **Device side**: platform firmware

- **Coordinator side**: control4_agent_voicecoordinator.c4z

- **Documented at**: coordinator version 139

## The model

>  The proxy carries configuration, never audio. The coordinator pushes each input device a connection URI per service; when the microphone button is held, the device streams straight to the URI of its current target. If the pushed state is stale, the microphone lights up and the audio goes nowhere.

Both ends of this interface ship with the platform. The device side is implemented in the firmware of devices that bind the `voiceinput.c4i` proxy; no packaged driver implements it, and a target driver never touches it directly. The coordinator side is the Voice Coordinator agent. The interface is still worth knowing precisely, because it is the half of the voice pipeline that explains most field failures: a target can be healthy while the input device holds a blank or stale config. This page pairs with the Voice Coordinator reference, which documents the target side.

The unit of configuration is the **service name**. An input device holds one stored configuration per service, and the coordinator addresses every push with a `TARGET_ID` parameter carrying that name. Which services an input device owns depends on the project:

**Services on an input device**

| Service name | Backed by | Attached |
| --- | --- | --- |
| comcast | Every transcoder-path voice target, third-party ones included. | Per room, following the room's selected watch device. |
| appletv | An apple-path voice target. | Per room, following the room's selected watch device. |
| siri | A per-device instance that piggybacks on an apple target's configuration. | Per device, when the system companion driver is in the project. |
| concierge | The concierge agent, through the daemon. | Per device, only on devices without a navigator or with the `has_concierge` capability. |
| avs | The far-field service class. Disabled at this version: its support check returns false for every device. | Never, currently. |
| "" | A null placeholder every device owns, used when no real service is available. | Always. |

## Call flow

_Diagram: Sequence of how a voice input device gets its configuration and uses it. Selection phase: the input device reports its room by setting the CurrentRoom proxy variable, which the coordinator listens for; when that room's selected watch device corresponds to a voice target with a valid address, the coordinator pushes the device SET_VOICE_TARGETS_DATA with the room's possible targets, SET_VOICE_INPUT_CONFIG carrying the target's c4vc URI keyed by service name, and SET_CURRENT_TARGET naming that service. URI change phase: when voiced reports a changed c4vc URI for a service, the coordinator pushes a new SET_VOICE_INPUT_CONFIG for it, and only when the value actually differs from what it last sent. Push-to-talk phase: the device streams microphone audio directly to the stored c4vc URI of its current target, and voiced relays the stream to the target; neither the proxy nor the coordinator is on the audio path._

_ Configuration flows down, audio flows around. **Ochre arrows are coordinator-initiated.** The device's only upstream channel is its proxy variables. _

## Conventions

### Two channels, one direction each

Commands flow from the coordinator to the device, sent with `C4:SendToDevice` to the voiceinput proxy's device id. State flows back from the device as proxy variable changes: the coordinator registers a listener on every variable in the table below and reads current values on demand. There is no request and reply pair anywhere on this interface.

### The connection gate

The coordinator sends a device nothing while the `IsConnected` variable is false. Sends attempted during that window are dropped, not queued; a device that comes online later receives current state on the next synchronization pass, not a replay.

### Capabilities are a UI request

Device capabilities on this interface come from a `GET_CAPABILITIES` UI request to the proxy device, answered with an XML capabilities block and cached for the life of the coordinator's device object. This is where the `has_navigator` and `has_concierge` flags that gate the concierge service come from.

### Declared surface versus exercised surface

The proxy vocabulary is larger than what the current coordinator actually sends. The commands documented individually below are exercised at this version. A second group is declared and dormant: `SET_REGISTRATION_DATA`, `CLEAR_REGISTRATION`, `SET_VOICE_INPUT_MODE`, `SET_TARGET_VIABLE`, `SET_TARGET_NAME`, `SET_WAKE_WORD`, `SET_VOICE_TARGET_DEVICE_CONFIG`, and `SET_WHOLEHOME_SERVICE_CONFIG` exist in the coordinator's vocabulary but have no live caller, mostly because the far-field service that used them is disabled. Do not build expectations on them.

## Commands to the device

>  All sent by the coordinator, all parameterized by service name in `TARGET_ID` unless noted. The device is expected to store per-service state and act on the current target.

SET_VOICE_INPUT_CONFIG Config push

Stores one service's connection URI on the device. This is the load-bearing command of the whole interface.

**Parameters**

| Name | Type | Notes |
| --- | --- | --- |
| CONFIG | string req | The service's `c4vc://` URI, or empty. Empty after a service has been configured means the service stopped; the device should drop the endpoint. |
| TARGET_ID | string req | Service name the config belongs to. |

**Send discipline.** The coordinator keeps a per-device, per-service cache of the last value sent and suppresses duplicates. It also defers pushing an empty config for a service that has never had a valid one, so slow startups do not erase state a device already holds.

CLEAR_VOICE_INPUT_CONFIG Config push

Removes one service's stored configuration entirely, as opposed to storing an empty value. The coordinator uses it when two services would collide on the same URI port on one device: when a service becomes current, any sibling sharing its port is cleared rather than blanked.

SET_CURRENT_TARGET Selection

Names the service the microphone should feed now. `TARGET_ID` is the service name. Sent when a room's selected watch device maps to a different target, when a config for the current service changes, and when a device joins a room that has an active target.

SET_DEFAULT_TARGET Selection

Names the fallback service used when no room target is selected. The coordinator assigns the default as services are attached, so the last attached service wins; when a room has no eligible target, the device is steered back to this default.

SET_TARGET_ENABLED Selection

Marks a service enabled on the device. The current coordinator sends it with `IS_ENABLED = "true"` as each service is attached; the disable direction is not exercised.

SET_CURRENT_ROOM_VOICE_TARGETS Room data

The room's eligible voice targets as a comma-separated list of proxy device ids in `TARGET_IDS`. The older of the two formats; kept for compatibility.

SET_VOICE_TARGETS_DATA Room data

The same information with structure: `TARGETS_DATA` is a JSON array covering both the room's target devices and the project-wide services. This is the preferred format, and it is where a device learns the service type it will see for each target. Note that every transcoder-path target reports the type `comcast` here.

SET_FEEDBACK_STATE Session state

Pushes a service's dialog state string in `STATE`. At this version it is exercised only as cleanup: when the current service changes, the outgoing service's state and response data are blanked.

SET_RESPONSE_DATA Session state

Pushes a service's response payload in `DATA`. Same situation as `SET_FEEDBACK_STATE`: currently sent only to blank state on a target switch.

## What the device reports

>  The device speaks by setting proxy variables. The coordinator listens to all of them, acts on one, and reads the rest as state.

**Voiceinput proxy variables**

| Id | Variable | Type | How the coordinator uses it |
| --- | --- | --- | --- |
| 1000 | IsMicEnabled | bool | Read as status. |
| 1001 | RegistrationData | string | Read back; written only by the dormant registration flow. |
| 1002 | IsRegistered | bool | Read as status. |
| 1003 | IsTargetEnabled | bool | Ignored at this version; the coordinator treats every device as target-enabled. |
| 1004 | VoiceInputConfig | string | Read back to inspect what the device holds. |
| 1005 | ResponseData | string | Read as status. |
| 1006 | FeedbackState | string | Read as status. |
| 1007 | VoiceInputMode | string | Read as status. |
| 1008 | IsConnected | bool | **The command gate.** Nothing is sent while false. |
| 1009 | CurrentTarget | string | Read as status. |
| 1010 | CurrentRoom | room id | **Acted on.** A change moves the device between rooms, which can retarget it and trigger a fresh round of pushes. |
| 1011 | CurrentRoomVoiceTargets | string | Read as status. |
| 1012 | TargetViable | bool | Read as status. |
| 1013 | DefaultTarget | string | Read as status. |
| 1014 | UserName | string | Unused at this version. |
| 1015 | SystemVoiceTargets | string | Unused at this version. |
| 1016 | TargetName | string | Unused at this version. |

Beyond variables, two messages travel from drivers toward the coordinator on adjacent paths. A voice input driver stack can send the agent `REQUEST_RESYNC_VOICE_INPUT_CONFIG` with the voiceinput proxy device id to force a full re-push past the dedup cache. And the coordinator's own proxy accepts `VOICE_TARGET_CONFIG_CHANGED`, which at this version is logged and otherwise ignored. Both are documented on the coordinator page.

## Data shapes

### The config URI

What `SET_VOICE_INPUT_CONFIG` carries is the first entry of the service's `c4vc_uris` from voiced, shaped `c4vc://<id>@<ip>:<port>/`. The device connects to it when the microphone is held. The coordinator treats it as opaque except for one detail: the port, which it extracts with the pattern `"@[^:]*:(%d+)"` to detect same-port collisions between services.

### TARGETS_DATA

```lua
[
  {
    "DeviceId": 321,             -- the target's proxy device id
    "ServiceType": "comcast",    -- as rewritten, not as the driver reported
    "Category": "target"
  },
  {
    "DeviceId": 13,              -- the agent's own device id
    "ServiceType": "concierge",
    "Category": "service",
    "DisplayName": "Concierge"
  }
]
```

Targets carry the **proxy** device id of the target device, not its protocol id. Service entries exist only while voiced is connected and managed, and only for services the user has enabled. The table is assembled from an unordered map, so entry order is not meaningful.

### GET_CAPABILITIES reply

An XML block of flag elements. The two the coordinator reads today:

```lua
<capabilities>
  <has_navigator>true</has_navigator>
  <has_concierge>false</has_concierge>
</capabilities>
```

## Behavior notes

#### The device follows its room, and the room picks the target

Retargeting is driven by the room's selected watch device, not by anything the input device chooses. When the selection maps to a voice target with a valid config, every input device in the room is switched to that target's service. A target with no config yet is skipped, so a half-started driver cannot displace a working target.

#### The dedup cache is unconfirmed

The coordinator's per-service cache records what it sent, not what the device accepted. A device that missed a push, or lost state on its own, stays out of sync until a value actually changes or a resync is forced with `REQUEST_RESYNC_VOICE_INPUT_CONFIG`. The symptom is a coordinator holding correct URIs while the device holds nothing.

#### One stored endpoint per URI port on some firmware

Some device firmware keys its stored voice endpoint by the c4vc port rather than by service name, so two services sharing a port overwrite each other no matter what is pushed. The coordinator works around it by clearing same-port siblings when a service becomes current, which preserves cross-port caching for fast target switching.

#### Empty is deferred, then meaningful

A service that has never produced a valid URI is not pushed at all. After the first valid URI, an empty `SET_VOICE_INPUT_CONFIG` is a deliberate statement that the service stopped or was disabled.

#### Room membership follows the variable, not Composer

The device's Composer room is only the starting point. The `CurrentRoom` variable wins, which is how a portable remote carries its voice targeting from room to room.

#### Joining a room re-asserts the room's target

A device entering a room with an active voice target is immediately attached to that service and made current. Entering a room without one falls back to the device's default service. Re-entry is not repeated on every synchronization pass, so a user's explicit service choice on the device survives resyncs.

#### Registration is designed in, switched off

The variables and commands for service registration (codes, registered flags, wake words) belong to the far-field service flow, which is disabled at this version. Expect them to stay inert until that class returns.

## Observing the exchange

Nothing on this interface has a UI, so debugging is reading state from both ends and comparing.

On the coordinator, raise `Log Level` to `3 - Info`: every `SET_VOICE_INPUT_CONFIG` send is logged with the device, the value, and the service, and an explicit warning marks every push of an empty config. The `Query Statuses` action prints each service instance and the URI it currently holds, which is the coordinator's side of the truth.

On the device, read the proxy variables: `VoiceInputConfig` and `CurrentTarget` say what the device actually holds and where the microphone will stream. When the two ends disagree, the dedup cache note above is usually the story, and a `REQUEST_RESYNC_VOICE_INPUT_CONFIG` for that device is the fix.

Voice input proxy, device contract reference Documented against Voice Coordinator version 139
