Connect a USB-CEC adapter
This guide takes one room from a new USB-CEC adapter to a TV that
wakes with the room’s Player. At the end, the adapter sends and
answers HDMI-CEC messages for the machine. A Play, or a press of the
room remote’s power button, wakes the TV and switches it to the
machine. The power
button also turns the TV off, and the TV’s own remote drives the
Player.
The room in this guide has a TV, an AV receiver on one of the TV’s
inputs, and the machine node-1 on one of the receiver’s inputs. The
first adapter equipment-operator supports is the Pulse-Eight
USB-CEC adapter
.
The Linux kernel drives it with its pulse8-cec driver, and the
kernel’s
CEC admin guide
describes that driver and the serial-line attachment it needs.
You need:
equipment-operator, from the install guide, withcec.yamlapplied.- The
display-operator, which publishes the machine’s HDMI output as aDisplaywith its CEC physical address. - For the wake and the power press: a
Receiverwhose input names the machine and thatDisplay, under aPlayerof themedia-operator. The install guide declares thatReceiver. - A spare HDMI input on the receiver, and an HDMI cable that carries the CEC wire.
kubectlwith cluster-admin access, because the guide edits aMachineand creates aDeviceClass.
Connect the adapter
Connect the adapter’s HDMI output to a spare input of the receiver. Leave the adapter’s own HDMI input empty. Connect the adapter’s USB plug to the machine. The machine’s video stays on its own cable to its own receiver input.
The adapter carries no video in this setup. It needs no video, because it announces the physical address of the machine’s video input, not of its own port. CEC is one wire that every HDMI port of the tree shares, so the adapter’s port does not matter to the other devices. Pulse-Eight documents this two-cable setup for 4K, because the adapter passes 4K at 60 Hz only in 4:2:0 color.
A drill measured this setup with an empty adapter input. A scan from the adapter found the TV, the receiver, the adapter itself, and a streaming player on another receiver input. A second cable from the machine into the adapter’s input changed nothing in the scan.
Many HDMI cables leave out the CEC wire, and such a cable fails with
no error of its own. In the drill, the first cable from the adapter to
the receiver carried no CEC. Every message from the adapter went
unacknowledged, and the adapter heard no traffic at all. Another
cable fixed it. When the CECBus below reports that no device
answers, try another cable first.
Attach the adapter to the machine
A liken machine loads only the drivers its Machine names, and the
adapter’s driver binds only after the machine attaches the adapter’s
serial line. Declare three modules in spec.modules, in this order,
and one spec.serio entry:
spec:
modules:
- cdc_acm
- serport
- pulse8_cec
serio:
- protocol: pulse8-cec
usb:
vendor: "2548"
product: "1002"
Keep the modules the machine already declares in the list. A merge
patch of spec.modules replaces the whole list. An added module and
an added entry take effect without a reboot. The liken guide
Load the drivers for a machine’s hardware
explains each field, and the
Machine
reference describes them.
Confirm the attachment:
kubectl get machine node-1 -o jsonpath='{.status.serio}' | jq
Attached is the good state, and nodes lists /dev/cec0 and the
remote’s event node. The machine then publishes two devices in its
ResourceSlice. The device whose name ends in -cec is the CEC bus,
and the cec-adapter DeviceClass of the operator selects it. The
device whose name ends in -input carries the TV remote’s keys, and a
media Remote claims it at the end of this guide.
Read the Display’s physical address
Each device on a CEC bus has a physical address, which is its path
from the TV. The TV is 0.0.0.0. A receiver on the TV’s input 1 is
1.0.0.0, and a machine on that receiver’s input 3 is 1.3.0.0. The
adapter has no EDID to read its own address from. So the adapter
announces the address of the machine’s Display, which the
display-operator reads from the EDID the receiver serves:
kubectl get display don-0070-denon-avr -o jsonpath='{.status.physicalAddress}'
The output is the dotted address, such as 1.3.0.0. The field is
absent on a DisplayPort cable, and while the monitor has never served
a valid address. The
Display
reference describes the PhysicalAddressCurrent condition, which
says whether the address is current or retained.
Listen to the bus
The equipment-operator-cec pod on node-1 starts when the machine
publishes the -cec device. No CECBus names the machine yet, so the
pod creates one in Listen, named after the machine, with the
equipment.liken.sh/discovered label. The pod’s log says so:
no CECBus names machine node-1; created CECBus node-1 in Listen, which sends nothing on the wire
In Listen the adapter claims no logical address and sends nothing.
It opens the bus as a monitor and reports each device that sends a
message. The mode exists so that a new adapter does not change the
room before a person allows it. A device that joins a CEC bus can
wake the TV and switch its input.
Read what the adapter heard:
kubectl get cecbus node-1 -o jsonpath='{.status.adapters}' | jq
Each entry under devices is one device the adapter heard. The list
is partial, because a full list needs polls, and a poll is a
transmission. On the drill’s bus, the TV and a streaming player each
polled every logical address about every 15 seconds, so the list
filled within a minute. The Scanned condition is False with the
reason Listening while the adapter hears devices. It is False
with the reason Silent while the adapter has heard no device, and
the message then says that the cable may not carry the CEC wire.
The monitor needs the CAP_NET_ADMIN capability, and cec.yaml
grants the pod that one capability. Control needs none.
Give the adapter control
Control is your consent. Declare a CECBus of your own, under a new
name, that names the machine and the Display:
apiVersion: equipment.liken.sh/v1alpha1
kind: CECBus
metadata:
name: living-room
spec:
mode: Control
adapters:
- machine: node-1
display: don-0070-denon-avr
A CECBus in Control must name a display for every adapter, and
the API server refuses one that does not. Do not apply your CECBus
under the discovered name, node-1. The node workload owns that
object’s spec. Your CECBus names the same machine, so the node
workload follows yours and deletes the one it created:
deleted the discovered CECBus node-1: CECBus living-room names machine node-1
the adapter on node-1 moves from node-1 to living-room
In Control the adapter claims a logical address as a playback
device. It announces the Display’s physical address and the OSD
name liken, which the TV shows in its list of sources. To show
another name, set spec.osdName to 1 to 14 printable ASCII
characters. A new name makes the adapter release its address and
claim it again, and the TV sees the source leave and come back.
The adapter scans the bus once when it joins. The scan polls every logical address and asks each device that answers for its name, vendor, CEC version, and power. After the scan, the adapter sends nothing on a timer. On one measured bus, a TV switched its own input in reply to an adapter that scanned every minute. So the adapter keeps its device list current from the messages it hears, and asks a device questions only when the device announces itself.
kubectl get cecbuses -o wide shows the result:
NAME MODE NAME MACHINES JOINED COHERENT AGE SCANNED STATE DEVICES
living-room Control liken node-1 True True 2m True Scanned TV,AVR-X1700H
The four conditions say what is wrong when the bus does not reach that state:
| Condition | False means |
Correction |
|---|---|---|
AddressKnown |
The reason NoAddress: the adapter has no physical address from its Display. |
Read the Display’s status.physicalAddress, and check the name in spec.adapters[].display. |
Joined |
The reason NoLogicalAddress: the adapter holds no logical address yet. |
Read status.adapters[].state and message. A refused call gives the kernel’s text there. |
Coherent |
The reason Apart: two adapters of the bus do not see each other. |
The adapters are on different wires. Check each adapter’s cable. |
Scanned |
The reason NoAnswer: no device answered the adapter’s polls. |
Connect another HDMI cable between the adapter and the receiver. |
An adapter entry older than 90 seconds makes each condition Unknown
with the reason Stale. A node workload that stopped makes each
condition False with the reason Stopped. The
CECBus
reference describes every
field.
Adopt the Television
When a CECBus in Control finds a TV, the operator creates a
Television with the bus’s name and the
equipment.liken.sh/discovered label:
kubectl get televisions
NAME BUS POWER DISPLAY PLAYER REACHABLE AGE
living-room living-room Standby True 1m
status.displays lists the Display the bus’s adapter names, and
via names the Receiver the picture passes through. The operator
sets via when a Receiver input names the Display’s machine and
the Display as its monitor. The receiver must also be above the
Display in the tree:
kubectl get television living-room -o jsonpath='{.status.displays}' | jq
To keep the Television in your configuration, apply your own under
the bus’s name:
apiVersion: equipment.liken.sh/v1alpha1
kind: Television
metadata:
name: living-room
spec:
cec:
bus: living-room
The object keeps the discovered label, and the operator keeps it for
as long as no other Television names the same bus. A Television
under another name that names the same bus also works. The operator
then deletes the discovered one.
Leave spec.power out unless you want to drive the TV by an edit. The
node workload applies each generation of the spec once, and it never
applies a generation again. So an object that you create with
spec.power, or an edit that adds it, turns the TV on or off once.
Quote the value, as power: "On", because kubectl reads an
unquoted On as the boolean true. The
Television
reference describes the
rules.
Reachable is True while the TV answers its power status. A TV in
a deep standby or an eco mode can stop answering. Then Reachable is
False with the reason NoPowerStatus, status.power is empty, and
the operator cannot wake that TV.
Wake the TV with the room
The TV wakes with the receiver. A Receiver session wakes the room
when a Play starts on it, when its screen wakes, or when the remote’s
power button turns the room on. A session that appears with a Play or
its screen already on also wakes the room. The operator then finds the Television
whose status.displays lists the session’s Display, and writes a
new status.session.wokeAt on it.
For each new wokeAt, the adapter that speaks for that Display runs
one wake:
- It reads the TV’s power, and sends Image View On until the TV reports On. One wake sends at most three such commands.
- When the TV reports On, or when those commands end without it, it
sends Image View On and Active Source for the
Display’s physical address. The TV and the receiver switch to the machine’s input. - For 30 seconds, it guards the input. When another source claims the input with Active Source, the adapter waits 2 seconds and claims it again, at most twice for one wake.
- At the end of the guard, it reads the TV’s power again and writes
the
WakeAppliedcondition.
The guard exists because another source can take the input after a wake. In the first drill, the receiver woke on a streaming player’s input, because the player claimed Active Source as the room woke. A second Active Source, sent once the room was on, switched the receiver to the machine. The adapter claims the input for a person and never against one. A person who picks another input on the receiver or in the TV’s source menu ends the guard at once.
kubectl get televisions -o wide shows the wake:
NAME BUS POWER DISPLAY PLAYER REACHABLE AGE SOURCE SESSION WAKE STANDBY APPLIED
living-room living-room On don-0070-denon-avr house/living-room True 1h 1.3.0.0 don-0070-denon-avr Confirmed
WakeApplied is True with the reason Confirmed when the TV
reported On and the last Active Source on the bus is the Display’s.
The other reasons:
Unconfirmed: the TV did not report On.SourceTaken: another source held the input at the end of the guard. When this repeats, that source claims the input after the guard ends. Check the CEC settings of that device.Chosen: a person picked another input during the guard.Refused: the adapter could not send.Stopped: the session’s sleep, a change of the bus, or a stop of the node workload ended the wake.Superseded: a new generation ofspec.power, or an Active Source from another adapter of the bus, ended the wake.TooLate: the wake had not started 2 minutes after the node workload saw it, and it sent nothing.
A press of the home key of the room remote writes
status.session.inputAsk with the action show. The adapter then
claims the TV’s input for the Display the same way, when the TV
reports On or ToOn, and the receiver selects the session’s input if
it is on. The ask wakes no TV that is off. A press of any other key,
except a power key, writes the action ensure while the Receiver’s
InputSelected condition is not True. An ensure asks a receiver
that is on for the session’s input, and sends the TV nothing.
While a session holds the room awake, the adapter also answers two
messages from other devices, for the session’s Display only. A TV
that comes out of standby broadcasts Request Active Source, and the
adapter answers with Active Source, so the TV shows the Player
instead of its own home screen. A TV whose source menu a person uses
broadcasts Set Stream Path with an input’s physical address, and the
adapter answers with Active Source when that address is the
Display’s. The adapter leaves a Request Active Source to another
source when a person moved the route to that source after the
adapter’s own claim. A Set Stream Path for the Display of a session
that sleeps wakes the Player’s screen, as the last section of this
guide describes.
An operator restart wakes nothing. Both workloads adopt the sessions they find when they start, and send nothing for them.
Turn the room off with the power button
The room remote’s power button turns the whole room on or off, the
TV included. A power ask in status.session.powerAsk names the
action toggle, on, or off. A toggle always moves the room. An
on moves it only when the room is off, and an off only when the
room is on. An ask that moves nothing sends nothing and writes one
log line that says so. At each press, the adapter asks the TV for its power,
because no timer asks the TV between presses. The press waits up to 3
seconds for the answer, and it decides from status.power when no
answer arrives. A TV that reports On or ToOn means the room is on, so
the press turns the room off. A TV in Standby or ToStandby means the
room is off, so the press wakes the room as above. With no
Television, or a TV that reports no power, the receiver’s power
decides.
A press that turns the room off writes status.session.standbyAt on
the Television. The adapter sends the TV Standby, reads the power
back, and writes the StandbyApplied condition. The receiver goes to
standby too, when it has a standby command. A WiiM has none, so it
stays on, and its log line says so.
Only the power button turns the TV off. A Play that ends, a screen
that goes idle, and an operator restart leave the TV as it is. The TV
can show another input, such as a streaming player, while the room’s
Player is idle.
The TV can also move the Player’s screen. When a person picks the
Player’s input in the TV’s source menu while the screen sleeps, the
screen wakes. When the bus carries a Standby while the session holds
the room awake, the screen sleeps, but only while nothing plays. A TV
sends Standby when a person turns it off with its own remote, and some
TVs send nothing then. The adapter writes each such ask in
the Television’s status.screenAsk, and the media-operator
relays it to the Player.
Read the TV remote’s keys
The kernel turns the TV remote’s buttons into key events on the
adapter’s input device. It does that only while the adapter holds a
logical address, so the bus must be in Control. In the first drill,
with the adapter as the active source, the arrows, OK, and back of
the TV’s remote arrived as KEY_UP,
KEY_DOWN, KEY_LEFT, KEY_RIGHT, KEY_OK, and KEY_EXIT. The
media-operator binds all six. That TV’s remote had no play or pause
key, so the drill did not test the transport keys.
equipment-operator does not read the keys. A media Remote claims
the adapter’s -input device, the same way it claims a Bluetooth
remote. A Remote names a DeviceClass that you write. This class
selects every TV remote input device that liken publishes:
apiVersion: resource.k8s.io/v1
kind: DeviceClass
metadata:
name: cec-remote
spec:
selectors:
- cel:
expression: |
device.driver == "liken.sh" &&
has(device.attributes["liken.sh"].subsystem) &&
device.attributes["liken.sh"].subsystem == "input"
Declare the Remote in the Player’s namespace:
apiVersion: media.liken.sh/v1alpha1
kind: Remote
metadata:
name: living-room-tv
namespace: house
spec:
device:
class: cec-remote
On a cluster with more than one adapter, add a selector on the
device’s address attribute, so the Remote claims one adapter’s
input device. Then name the Remote in the Player’s
spec.remotes. The media-operator guide
Map a new controller
finds the codes the other buttons send and maps them.
When the adapter is unplugged, the equipment-operator-cec pod exits,
and the kubelet starts it again when the adapter returns to the same
USB port. The Remote’s pod keeps running and reads nothing, because
a running container never receives the new event node. Delete the
pod, and the media-operator creates it again with the new node:
kubectl delete pod -n house living-room-tv-remote
When the TV does not wake
“The TV did not wake” crosses five objects. Read each one in this order, and stop at the first one that is wrong:
- The
Machine’sstatus.serio: the adapter isAttached, and its nodes exist. - The
Display’sstatus.physicalAddress: the machine’s place in the tree. - The
CECBus’s conditions andstatus.adapters: the adapter holds a logical address and announces the address. - The
Television’sReachablecondition andstatus.power: the TV answers, and what it reports. - The
Television’sstatus.session,status.wokeAt, andWakeApplied: the session reached the TV, the adapter ran the wake, and what the bus reported at the end of it.
A Television with no status.session gets no wake. Check that the
Receiver’s input names the same Display as the CECBus’s
adapter, and that the Television’s status.displays lists it. A TV
that is connected straight to the machine, with no Receiver, has no
session to follow, and it does not wake with the room.
Each change of a condition of a CECBus or a Television posts one
Kubernetes Event with the condition’s reason and message, so
kubectl describe shows what changed in the last hour. A reason that
a person may need to act on is a Warning: NotReported, Refused,
NoAddress, NoLogicalAddress, Apart, NoAnswer, Stale,
NoBus, NotFound, NoPowerStatus, Unconfirmed, SourceTaken,
and TooLate. Every other reason is Normal. When discovery creates or
deletes a Television, the operator posts TelevisionCreated or
TelevisionDeleted on its CECBus. A CECBus and a Television
are cluster-scoped, so their Events are in the default namespace,
and kubectl events finds them only with -n default or -A:
kubectl describe television living-room
kubectl events -n default --for television/living-room
The API server deletes an Event one hour after its last change. The
conditions and the logs keep the facts after that.
The node workload writes one log line for each wake, standby, and power read. It also writes one line for each message that a person notices, such as Active Source, Routing Change, Image View On, or Standby:
kubectl get pods -n liken-system -l app=equipment-operator-cec -o wide
kubectl logs -n liken-system <pod on node-1>
Check the wire by hand
cec-ctl
, from
v4l-utils
, checks the cable
and the tree without the operator. The adapter allocates to one claim
at a time, so first take the node workload off the machine:
kubectl label node node-1 equipment.liken.sh/cec=none
Run a pod that claims the adapter through the same
ResourceClaimTemplate, on the same machine:
apiVersion: v1
kind: Pod
metadata:
name: cec-check
namespace: liken-system
spec:
nodeSelector:
kubernetes.io/hostname: node-1
tolerations:
- operator: Exists
containers:
- name: cec
image: debian:trixie-slim
command: ["sh", "-c", "apt-get update -qq && apt-get install -qq -y v4l-utils && exec sleep infinity"]
securityContext:
capabilities:
add: ["NET_ADMIN"]
resources:
claims:
- name: adapter
resourceClaims:
- name: adapter
resourceClaimTemplateName: cec-adapter
Claim a playback address with the Display’s physical address, list
the tree, and ask the TV for its power:
kubectl exec -n liken-system cec-check -- cec-ctl --playback --osd-name liken --phys-addr 1.3.0.0
kubectl exec -n liken-system cec-check -- cec-ctl --show-topology
kubectl exec -n liken-system cec-check -- cec-ctl --to 0 --give-device-power-status
A topology that lists only the adapter, and a power request that the
TV does not acknowledge, point to a cable without the CEC wire. To
watch all traffic on the wire, which needs the NET_ADMIN capability
above, run cec-ctl --monitor-all. To wake the TV by hand, send
cec-ctl --to 0 --image-view-on.
When you are done, release the adapter, delete the pod, and remove the label, so the node workload returns:
kubectl exec -n liken-system cec-check -- cec-ctl --clear
kubectl delete pod -n liken-system cec-check
kubectl label node node-1 equipment.liken.sh/cec-