Remote

A Remote is one physical controller: the device it is and, where its model needs one, the Keymap 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 Remotes 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
device object yes The controller itself, selected out of the devices the hardware operators publish, and the parameters its driver prepares it with.
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.
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
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.
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.
parameters object 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
driver string yes The driver the parameters are for, such as bluetooth.liken.sh.
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
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.
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.
unbound []object 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
code integer yes The raw evdev code, the number the controller reports on the wire.
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.
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 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 ), 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 ), 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 ), 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 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.