
<!-- Generated from deploy/remotes-crd.yaml by crdref. Do not edit. -->

# `Remote`

A `Remote` is one physical controller: the device it is and, where
its model needs one, the [`Keymap`](/docs/reference/keymaps/) for its
model. The base table already gives a `Remote` with no `Keymap` the
arrows, OK, back, and every `KEY_*` code the device emits, and a
`Keymap` corrects a device the base gets wrong. It names no
player. A `Player` names the `Remote`s it owns through
`spec.remotes`, so the unit that owns a controller is the one that
lists it, and one controller can drive several units.

A `Remote` name is at most 32 characters, because `media-operator` builds the names
of the objects it creates from it, and Kubernetes limits some of those
names to 63 characters.

    apiVersion: media.liken.sh/v1alpha1
    kind: Remote
    metadata:
      name: den-pad
      namespace: den
    spec:
      device:
        class: gamepad
        selector: device.attributes["bluetooth.liken.sh"].address == "04:4A:5B:11:22:33"
      keymap: dualsense

`spec.device.parameters` is opaque configuration for the driver that
prepares the controller. A `Remote` uses it to name the classes of
input it needs, in the driver's own words. The bluetooth operator's
manual documents its `inputs` parameter and the classes it accepts.

    spec:
      device:
        class: gamepad
        selector: device.attributes["bluetooth.liken.sh"].address == "7C:66:EF:22:E7:80"
        parameters:
          driver: bluetooth.liken.sh
          values:
            inputs: [joystick]

One physical controller, selected by its device and mapped by the default mapping and, where its model needs one, by its Keymap. A Player names the Remotes it owns; the Remote names no player.

## spec

The controller, and the Keymap for its model where its model needs one.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| <span id="spec--device"></span>`device` | [object](#specdevice) | yes | The controller itself, selected out of the devices the hardware operators publish, and the parameters its driver prepares it with. |
| <span id="spec--keymap"></span>`keymap` | string | no | The Keymap for this controller's model, by name. A Keymap is cluster-scoped, so the name has no namespace. The field is optional and rarely needed: the default mapping already passes every KEY_* code and turns the hats into the arrows, so a Keymap is for a device the kernel names wrongly. A device maps one way on every unit, as it does under hwdb. |
| <span id="spec--discovery"></span>`discovery` | boolean | no | Discovery mode, for mapping a controller that has no Keymap yet. The Remote's pod keeps every input node the claim delivered and logs each event the way a Keymap names it, so a person presses every button and reads the codes out of the pod log. The pod still publishes keys in discovery exactly as it does outside it, so the controller keeps driving its unit while a person maps it. Turning discovery on or off replaces the Remote's pod, which drops controller input for a few seconds. A pod in discovery reads every event the claim delivers. Outside discovery the pod reads only the keys and hats it publishes. |

### spec.device

The controller itself, selected out of the devices the hardware operators publish, and the parameters its driver prepares it with.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| <span id="specdevice--class"></span>`class` | string | yes | The DeviceClass the claim allocates through. Consumer classes are the cluster owner's vocabulary, so the name is whatever this cluster calls its controllers. |
| <span id="specdevice--selector"></span>`selector` | string | no | A CEL expression over device.attributes that picks this one controller, such as a match on its address. Omit it, and the class alone chooses. |
| <span id="specdevice--parameters"></span>`parameters` | [object](#specdeviceparameters) | no | Opaque configuration for the driver that prepares the controller, carried onto the claim unread. This is where a Remote names the classes of input it needs. The bluetooth operator's manual documents its inputs parameter. |

#### spec.device.parameters

Opaque configuration for the driver that prepares the controller, carried onto the claim unread. This is where a Remote names the classes of input it needs. The bluetooth operator's manual documents its inputs parameter.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| <span id="specdeviceparameters--driver"></span>`driver` | string | yes | The driver the parameters are for, such as bluetooth.liken.sh. |
| <span id="specdeviceparameters--values"></span>`values` | object | no | The parameters themselves. The driver defines them, and this operator copies them to the claim. |

## status

What the operator reports about this controller: the unit its presses reach now, the bonded device its claim allocated, and the declared codes its Keymap leaves unbound.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| <span id="status--player"></span>`player` | string | no | The Player this Remote's focus mark names now: the unit its presses reach, idle or playing. It is empty while no Player lists this Remote. |
| <span id="status--peripheral"></span>`peripheral` | string | no | The Peripheral for the device this Remote's claim allocated. The name is the device's address in lowercase with dashes. Read that object for the link and the battery level. The field is empty while the claim has no allocation, and for a controller another driver publishes. |
| <span id="status--unbound"></span>`unbound` | [\[\]object](#statusunbound) | no | Every code that this controller declares and its Keymap does not bind. The list holds only the unbound codes, not every code. A controller whose Keymap binds every declared code reports nothing here, and the field is absent until the Remote's pod has reported. The list shrinks as the Keymap grows, so it measures a mapping's progress during discovery and provides a completeness check after discovery. |

### status.unbound[]

Every code that this controller declares and its Keymap does not bind. The list holds only the unbound codes, not every code. A controller whose Keymap binds every declared code reports nothing here, and the field is absent until the Remote's pod has reported. The list shrinks as the Keymap grows, so it measures a mapping's progress during discovery and provides a completeness check after discovery.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| <span id="statusunbound--code"></span>`code` | integer | yes | The raw evdev code, the number the controller reports on the wire. |
| <span id="statusunbound--name"></span>`name` | string | no | The evdev name for the code, which is the name a Keymap binds it by. It is empty when the kernel gives the code no name, and such a code cannot be bound. |
| <span id="statusunbound--type"></span>`type` | string | yes | Which event type carries the code: key for a button, abs for a hat axis. One of: `key`, `abs`. |

## The Remote's pod

The operator reconciles one pod for every `Remote` in the
cluster, whether or not a `Player` names it. The pod holds the
controller's claim, reads its evdev nodes directly, folds the base
table with the `Remote`'s `Keymap`, publishes each event under the
kernel's key name, and synthesises the repeat stream for a control
that does not autorepeat. That work runs beside the device because
that is where hwdb runs on any Linux machine, and one pod then serves
every consumer at once.

The pod asks the kernel for only the keys and the hats it publishes,
so a device that reports motion while it rests costs the pod nothing.
The driver decides which events reach the node, and the claim's
`parameters` configure that choice. A pod in `spec.discovery`
asks for no narrowing and reads every event the claim delivers.

The claim tolerates the `bluetooth.liken.sh/disconnected`
taint with no time limit, so a controller that sleeps keeps its
allocation and the pod keeps running. It does not tolerate
`bluetooth.liken.sh/no-input-node`, so the pod stays `Pending` until
the controller first connects, then keeps running through every later sleep.

## Status

The operator writes three facts on a `Remote`. `status.player` is the
`Player` its focus mark names now. `status.peripheral` names the
`Peripheral` for the device the `Remote`'s claim allocated, which is
where a person reads the controller's link and its battery level.
`status.unbound` is the gap: every
declared control that the base table and the `Keymap` together map to
nothing, or map to `none`. A declared `KEY_*` code passes as itself,
so it is never unbound, and a keyboard remote starts with an empty
list. `kubectl get remotes` shows each controller's `Keymap`, the unit
it drives, its `Peripheral`, and its age. A `Keymap` that does not
compile is logged by the operator, and the `Remote` keeps its last
good table.

## Events

The operator posts a `KeymapRefused` Warning on the `Remote` when its
`Keymap` does not compile. The message gives the compiler's words, and
the last good key table stays on the bus. The operator posts it once
for as long as the same refusal stands. `kubectl describe remote`
prints it.

## On the bus

Each `Remote` owns one branch of the [bus](/docs/reference/bus/)
topic tree, `remotes/<namespace>/<name>/`, under the cluster's topic
base. The bus page gives the rules every topic follows and lists
every writer and reader of each.

| topic          | writer       | retained | carries                       |
|----------------|--------------|----------|-------------------------------|
| `events`       | the `Remote`'s pod | no       | one key event                 |
| `keys`         | operator     | yes      | the controller's key table    |
| `codes`        | the `Remote`'s pod | yes      | the declared code set         |
| `availability` | the `Remote`'s pod | yes      | `online` or `offline`         |
| `focus`        | operator     | yes      | the name of the `Player` it drives |
| `focus/cycle`  | the focus holder | no   | a request to advance focus    |

### events

The `Remote`'s pod publishes each event under the kernel's name for
the control, after it folded the base table with the `Keymap`:

    {"key": "KEY_UP", "value": 1}

`value` is the kernel's: 0 is the release, 1 the press, and 2 the
autorepeat. A keyboard's own autorepeat passes through, and the pod
synthesises value 2 for a gamepad button or a hat with a `repeat`
block. A control the folded table maps to nothing is not published. The
topic is not retained
([why](/docs/reference/bus/#retained-state-and-events)), so a
subscriber that joins later reads no stale press.

The operator reads the volume keys here: `KEY_VOLUMEUP` and
`KEY_VOLUMEDOWN` on each press and repeat, and `KEY_MUTE` and
`KEY_UNMUTE` on each press. It sets the level of the unit the
controller's focus mark names
([how](/docs/reference/players/#volume)), so neither the playback pod
nor the idle client handles them.

### keys

The controller's key table, as the operator compiled it: the base
folded with the `Remote`'s `Keymap`, one row per control, with the
evdev type, code, and value on the left and the key name on the
right, and the repeat delay and interval in milliseconds where a row
repeats:

    [{"type": 1, "code": 304, "value": 1, "key": "KEY_ENTER"},
     {"type": 3, "code": 17, "value": -1, "key": "KEY_UP",
      "repeatDelay": 400, "repeatInterval": 250}]

The operator is the only writer, and the topic is retained, so the pod
reads the current table the instant it connects and a `Keymap` edit
reaches it with no pod restart. The operator republishes only when the
table changes, and it clears the topic with an empty payload when the
`Remote` is deleted.

### codes

The codes the controller declares, read from its nodes' capability
bitmaps at every node open:

    {"keys": [304, 305], "axes": [16, 17]}

The set is complete with no button pressed, because the bitmaps
state every code a node can report. The topic is retained
([why](/docs/reference/bus/#retained-state-and-events)), and the pod
clears it with an empty payload when the nodes vanish. The operator
subtracts the folded table from the set and reports the gap as
`status.unbound` on the `Remote`.

### availability

`online` or `offline`, retained, the
[availability](/docs/reference/bus/#availability) signal for the
codes above. The `Remote`'s pod names this topic as its MQTT Last
Will with `offline` as the payload, and publishes `online` once it
connects. When the pod dies, the broker writes `offline`, so the
retained codes a dead pod left behind do not read as a live
declaration.

### focus and focus/cycle

The focus mark is the plain name of the `Player` this controller
drives now, as bytes, not JSON. The operator is the only writer,
and the topic is retained, so a press reaches its unit even while
the operator is down, except a volume key, which the operator
reads itself. Every reader of the controller's presses gates on the
mark. The playback pod's command sidecar acts only when
the mark names the `Player` its film runs on. An idle unit's sidecar
acts only when the mark names that `Player` itself, and the idle
screen draws a small hexagon beside the focused controller in its
parts list.

The operator moves the marks. When a `Play` starts on a `Player`,
each of that unit's controllers is marked to it, so the controller
in a person's hand drives the film they just started. A mark that
names a deleted `Player`, or a `Player` that no longer lists the
controller, moves to the first bound `Player` by name. A `Play`
that finishes moves no mark: the unit stays focused and shows its
idle screen. A controller that no `Player` lists any more has nothing
to drive, so the operator clears its mark with an empty retained
payload.

A press of `KEY_CYCLEWINDOWS` publishes on `focus/cycle`, with an
empty payload: the topic names the controller, and the request
carries nothing else. Only the holder of focus publishes it, the
playback pod's command sidecar during a film and the idle screen
client between films. The operator
reads the request and advances the mark to the next bound `Player`
by name, wrapping the last back to the first. A controller bound to
one unit wraps to the same `Player`, and the operator republishes the
mark, which the idle screen answers with a pulse of its hexagon.

The idle screen acts on a mark only when a person caused it. A live
mark that moves to its `Player` wakes the screen and pulses the
hexagon, and so does the repeat that answers the screen's own cycle
request. Any other repeat of the mark the screen holds changes
nothing, so a publisher that sends the same mark again does not wake
a sleeping screen.

The operator does not publish a mark again when it restarts. The
broker keeps each retained mark across the operator's restart and
delivers it on the new session. A restarted broker keeps no mark, so
after each new session the operator waits two seconds for the
broker's retained marks. It then publishes each mark it holds that
the broker did not deliver.

