A Remote is one physical controller: the device it is and the
Keymap for its model. 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.
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
One physical controller, selected by its device and mapped by its Keymap. A Player names the Remotes it owns; the Remote names no player.
spec
The controller and the Keymap for its model.
| Field | Type | Required | Description |
|---|---|---|---|
device |
object | yes | The controller itself, selected out of the devices the hardware operators publish. There are no parameters: nothing prepares an input device, and its nodes are read as they are. |
keymap |
string | yes | The Keymap for this controller’s model, by name. A Keymap is cluster-scoped, so the name carries no namespace. A Player entry in spec.remotes may override it per unit, so one controller can map two ways on two units. |
spec.device
The controller itself, selected out of the devices the hardware operators publish. There are no parameters: nothing prepares an input device, and its nodes are read as they are.
| 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. |
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, and publishes
to the bus. 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.
No status
The operator reports nothing on a Remote, so there is no status
subresource. kubectl get remotes shows each controller’s Keymap
and its age. A Remote whose Keymap does not compile shows the
failure on the Play that uses it.
On the bus
Each Remote owns one branch of the bus
topic tree, remotes/<namespace>/<name>/, under the cluster’s topic
base.
| topic | writer | retained | carries |
|---|---|---|---|
events |
the Remote’s pod |
no | one evdev event |
presence |
the Remote’s pod |
yes | {"connected": true} |
availability |
the Remote’s pod |
yes | online or offline |
focus |
operator | yes | the name of the owning Play |
focus/cycle |
translator | no | a request to advance focus |
events
The Remote’s pod publishes each event as the controller’s own evdev
numbers, untranslated:
{"type": 1, "code": 304, "value": 1}
type 1 is EV_KEY and type 3 is EV_ABS. code 304 is
BTN_SOUTH, and value 1 is the press, 0 the release. The pod
publishes only the events a Keymap can bind: every key code, and
the two hat axes. The keymap stays off this topic, so one Remote
can feed two players that map it differently. A press is an event
and not a state, so the topic is not retained and a subscriber that
joins later reads no stale press.
presence
Whether the controller’s event nodes are open right now:
{"connected": true}
The Remote’s pod reads the controller’s evdev nodes directly:
they open when the controller connects and vanish when it sleeps,
so presence comes straight from the device. The topic is retained, so the
operator reads the current value the instant it subscribes, and it
folds the flag into the Player status
it publishes.
availability
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
presence a dead pod left behind does not read as a connected
controller.
focus and focus/cycle
The focus mark is the plain name of the Play that owns this
controller now, as bytes, not JSON. The operator is the only writer,
and the topic is retained, so a press reaches its Play even while
the operator is down. Each translator for the controller gates on
the mark: it acts on a press only when the mark names its own
Play, and it drops every other press.
A press bound to cycle-focus publishes on focus/cycle. Only the
translator that holds focus publishes it, and the operator reads it
and advances the mark to the next unit. The operator also moves a
mark off a Play that finished, so a controller with a live Play
always has a translator that acts.