Consumer Electronics Control (CEC) is a feature of HDMI that allows devices
to control each other. I use it to set my TV to standby mode when my living
room PC is suspended and turn the TV back on when the PC is resumed.
Graphics cards don’t generally support CEC, but there is a solution to this
problem.
The Pulse-Eight USB - CEC Adapter functions as an HDMI passthrough and
adds CEC support to a PC via USB.
The common way to interface with this adapter is libCEC and the cec-client
program.
Let’s see how long it takes to send standby and power on commands to a TV.
$ echo "standby 0" | time cec-client --log-level 1 --single-command
log level set to 1
opening a connection to the CEC adapter...
real 0m 3.31s
user 0m 0.00s
sys 0m 0.01s
$ echo "on 0" | time cec-client --log-level 1 --single-command
log level set to 1
opening a connection to the CEC adapter...
real 0m 3.31s
user 0m 0.01s
sys 0m 0.00s
Changing the power state of the TV takes 3.3 s.
This means that when the PC is resumed, the TV will turn on over 3 seconds after
the system is ready.
In practice this significantly increases frustration when starting to use the PC.
In the case of my setup, the TV turns on after around 11 seconds after pressing
a key to resume the PC.
This means that it takes around 8 seconds until the PC is ready and 3 more
seconds until the TV is on.
I could not believe that it has to take whole 3 seconds to send a command
through the USB adapter.
If I reduced this time to a negligible amount, the total system resume time
would be cut by around 30%, which would be a significant improvement for me.
Time to get hacking!
Investigating cec-client
I wanted to check why exactly it takes so long to send a command.
I ran cec-client with debug logs enabled.
Debug log of cec-client
$ echo "on 0" | cec-client --single-command
opening a connection to the CEC adapter...
DEBUG: [ 1] Broadcast (F): osd name set to 'Broadcast'
DEBUG: [ 1] connection opened, clearing any previous input and waiting for active transmissions to end before starting
DEBUG: [ 6] communication thread started
DEBUG: [ 61] usbcec: enabling controlled mode
NOTICE: [ 232] connection opened
DEBUG: [ 232] << Broadcast (F) -> TV (0): POLL
DEBUG: [ 232] processor thread started
TRAFFIC: [ 232] << f0
DEBUG: [ 232] usbcec: updating line timeout: 3
DEBUG: [ 372] >> POLL sent
DEBUG: [ 372] TV (0): device status changed into 'present'
DEBUG: [ 372] << requesting vendor ID of 'TV' (0)
TRAFFIC: [ 372] << f0:8c
TRAFFIC: [ 619] >> 0f:87:08:00:46
DEBUG: [ 619] TV (0): vendor = Sony (080046)
DEBUG: [ 619] expected response received (87: device vendor id)
DEBUG: [ 620] registering new CEC client - v6.0.2
DEBUG: [ 620] >> TV (0) -> Broadcast (F): device vendor id (87)
DEBUG: [ 674] usbcec: autonomous mode = enabled
DEBUG: [ 730] usbcec: logical address = Recorder 1
DEBUG: [ 785] usbcec: device type = recording device
DEBUG: [ 790] usbcec: logical address mask = 206
DEBUG: [ 860] usbcec: physical address = 3000
DEBUG: [ 915] usbcec: auto power on = disabled
DEBUG: [ 915] SetClientVersion - using client version '6.0.2'
NOTICE: [ 915] setting HDMI port to 1 on device TV (0)
DEBUG: [ 915] SetConfiguration: double tap timeout = 200ms, repeat rate = 0ms, release delay = 500ms
DEBUG: [ 916] detecting logical address for type 'recording device'
DEBUG: [ 916] trying logical address 'Recorder 1'
DEBUG: [ 916] << Recorder 1 (1) -> Recorder 1 (1): POLL
TRAFFIC: [ 916] << 11
DEBUG: [ 1000] CEC transmission - received response - TRANSMIT_FAILED_ACK
TRAFFIC: [ 1000] << 11
DEBUG: [ 1085] CEC transmission - received response - TRANSMIT_FAILED_ACK
DEBUG: [ 1085] >> POLL not sent
DEBUG: [ 1085] using logical address 'Recorder 1'
DEBUG: [ 1085] Recorder 1 (1): device status changed into 'handled by libCEC'
DEBUG: [ 1085] Recorder 1 (1): power status changed from 'unknown' to 'on'
DEBUG: [ 1085] Recorder 1 (1): vendor = Pulse Eight (001582)
DEBUG: [ 1085] Recorder 1 (1): CEC version 1.4
DEBUG: [ 1085] AllocateLogicalAddresses - device '0', type 'recording device', LA '1'
DEBUG: [ 1085] usbcec: updating ackmask: 0002
DEBUG: [ 1140] Recorder 1 (1): osd name set to 'CECTester'
DEBUG: [ 1140] Recorder 1 (1): menu language set to 'eng'
DEBUG: [ 1140] GetPhysicalAddress - trying to get the physical address via ADL
DEBUG: [ 1140] GetPhysicalAddress - ADL returned physical address 0000
DEBUG: [ 1140] GetPhysicalAddress - trying to get the physical address via nvidia driver
DEBUG: [ 1140] GetPhysicalAddress - nvidia driver returned physical address 0000
DEBUG: [ 1140] GetPhysicalAddress - trying to get the physical address via drm files
DEBUG: [ 1140] GetPhysicalAddress - drm files returned physical address 3000
DEBUG: [ 1140] using auto-detected physical address 3000
DEBUG: [ 1140] Recorder 1 (1): physical address changed from ffff to 3000
DEBUG: [ 1140] << Recorder 1 (1) -> broadcast (F): physical address 3000
TRAFFIC: [ 1140] << 1f:84:30:00:01
NOTICE: [ 1305] CEC client registered: libCEC version = 6.0.2, client version = 6.0.2, firmware version = 12, firmware build date: Tue Apr 28 14:20:49 2020 +0000, logical address(es) = Recorder 1 (1) , physical address: 3.0.0.0, git revision: <unknown>, compiled on 2022-11-22 19:33:35 by buildozer@localhost on Linux 5.15.12-0-lts (x86_64), features: P8_USB, DRM, P8_detect, randr, Linux
DEBUG: [ 1305] << Recorder 1 (1) -> TV (0): OSD name 'CECTester'
TRAFFIC: [ 1305] << 10:47:43:45:43:54:65:73:74:65:72
TRAFFIC: [ 1339] >> 01:46
DEBUG: [ 1339] >> TV (0) -> Recorder 1 (1): give osd name (46)
DEBUG: [ 1616] << requesting power status of 'TV' (0)
TRAFFIC: [ 1616] << 10:8f
DEBUG: [ 1616] << Recorder 1 (1) -> TV (0): OSD name 'CECTester'
TRAFFIC: [ 1671] << 10:47:43:45:43:54:65:73:74:65:72
TRAFFIC: [ 2042] >> 01:8c
DEBUG: [ 2042] << Recorder 1 (1) -> TV (0): vendor id Pulse Eight (1582)
TRAFFIC: [ 2042] << 1f:87:00:15:82
DEBUG: [ 2042] >> TV (0) -> Recorder 1 (1): give device vendor id (8C)
TRAFFIC: [ 2112] >> 01:90:00
DEBUG: [ 2112] TV (0): power status changed from 'unknown' to 'on'
DEBUG: [ 2112] expected response received (90: report power status)
DEBUG: [ 2112] >> TV (0) -> Recorder 1 (1): report power status (90)
NOTICE: [ 2112] << powering on 'TV' (0)
TRAFFIC: [ 2112] << 10:04
DEBUG: [ 2305] unregistering all CEC clients
NOTICE: [ 2305] unregistering client: libCEC version = 6.0.2, client version = 6.0.2, firmware version = 12, firmware build date: Tue Apr 28 14:20:49 2020 +0000, logical address(es) = Recorder 1 (1) , physical address: 3.0.0.0, git revision: <unknown>, compiled on 2022-11-22 19:33:35 by buildozer@localhost on Linux 5.15.12-0-lts (x86_64), features: P8_USB, DRM, P8_detect, randr, Linux
DEBUG: [ 2305] Recorder 1 (1): power status changed from 'on' to 'unknown'
DEBUG: [ 2305] Recorder 1 (1): vendor = Unknown (000000)
DEBUG: [ 2305] Recorder 1 (1): CEC version unknown
DEBUG: [ 2305] Recorder 1 (1): osd name set to 'Recorder 1'
DEBUG: [ 2305] Recorder 1 (1): device status changed into 'unknown'
DEBUG: [ 2305] usbcec: updating ackmask: 0000
DEBUG: [ 2360] usbcec: disabling controlled mode
DEBUG: [ 2415] unregistering all CEC clients
DEBUG: [ 3247] communication thread ended
There’s a lot of stuff there.
First, notice the number before each log message.
It’s the number of milliseconds elapsed since the program started.
This can be used to pinpoint parts that take the most time to execute.
We can see that the power on command is not sent until over 2 seconds (!) into
program execution.
So what is going on before that?
There are things like retrieving the vendor ID and power status of
the TV and setting the OSD name.
This takes a lot of time and is useless to me.
After examining the source code of cec-client it looked like most of this stuff
happens in ICECAdapter::Open
.
Another conclusion was that the code is very complicated and hard to read.
There are abstractions over abstractions, and it gets difficult to follow the
execution path.
I needed another way.
Reverse engineering the protocol
While looking at the source code for libCEC and cec-client I noticed that the
adapter behaves like a serial port and is present in my system as /dev/ttyACM0
.
A quick search for baud in libCEC revealed that the baudrate is 38400.
The next task was to sniff the serial communication between cec-client and the
adapter.
One internet search later I had found slsnif and
Andrew Ruder’s fork, which looked like just the tool I needed.
I built and ran slsnif and then ran cec-client.
$ slsnif --nolock --log log --speed 38400 --unix98 /dev/ttyACM0 &
Serial Line Sniffer. Version 0.4.4
Copyright (C) 2001 Yan Gurtovoy (ymg@dakotacom.net)
Started logging data into file 'log'.
Opened pty: /dev/pts/0
Failed to open temp. file to save opened pty name, continuing...: Permission denied
Opened port: /dev/ttyACM0
Baudrate is set to 38400 baud.
$ echo "on 0" | cec-client --log-level 1 --single-command /dev/pts/0
log level set to 1
opening a connection to the CEC adapter...
Synchronizing ports...Done!
$ kill %1
[1]+ Terminated ./slsnif --nolock --log log --speed 38400 --unix98 /dev/ttyACM0
Log contents
Host --> (255) <SOH> (001) (254)
Device --> (255) <BS> (008) <SOH> (001) (254)
Host --> (255) <NAK> (021) (254)
Device --> (255) <NAK> (021) <NUL> (000) <FF> (012) (254)
Host --> (255) <CAN> (024) <SOH> (001) (254)
Device --> (255) <BS> (008) <CAN> (024) (254)
Host --> (255) <ETB> (023) (254)
Device --> (255) <ETB> (023) ^ (094) (168) ; (059) (193) (254)
Host --> (255) ( (040) (254)
Device --> (255) ( (040) <SOH> (001) (254)
Host --> (255) <CR> (013) <ETX> (003) (254)
Device --> (255) <BS> (008) <CR> (013) (254)
Host --> (255) <SO> (014) <NUL> (000) (254) (255) <FF> (012) (240) (254)
Device --> (255) <BS> (008) <SO> (014) (254) (255) <BS> (008) <FF> (012) (254)
Device --> (255) <DLE> (016) (254)
Host --> (255) <SO> (014) <NUL> (000) (254) (255) <VT> (011) (240) (254) (255) <FF> (012) (140) (254)
Device --> (255) <BS> (008) <SO> (014) (254) (255) <BS> (008) <VT> (011) (254) (255) <BS> (008) <FF> (012) (254)
Device --> (255) <DLE> (016) (254)
Device --> (255) E (069) <SI> (015) (254)
Device --> (255) F (070) (135) (254)
Device --> (255) F (070) <BS> (008) (254)
Device --> (255) F (070) <NUL> (000) (254)
Device --> (255) (198) F (070) (254)
Host --> (255) <EM> (025) (254)
Device --> (255) <EM> (025) <SOH> (001) (254)
Host --> (255) <ESC> (027) (254)
Device --> (255) <ESC> (027) <SOH> (001) (254)
Host --> (255) ! (033) (254)
Device --> (255) ! (033) <SOH> (001) (254)
Host --> (255) <GS> (029) (254)
Device --> (255) <GS> (029) <STX> (002) <ACK> (006) (254)
Host --> (255) % (037) (254)
Device --> (255) % (037) C (067) E (069) C (067) T (084) e (101) s (115) t (116) e (101) r (114) (254)
Host --> (255) <US> (031) (254)
Device --> (255) <US> (031) 0 (048) <NUL> (000) (254)
Host --> (255) * (042) (254)
Device --> (255) * (042) <NUL> (000) (254)
Host --> (255) <SO> (014) <NUL> (000) (254) (255) <FF> (012) <DC1> (017) (254)
Device --> (255) <BS> (008) <SO> (014) (254) (255) <BS> (008) <FF> (012) (254)
Device --> (255) <DC2> (018) (254)
Host --> (255) <SO> (014) <NUL> (000) (254) (255) <FF> (012) <DC1> (017) (254)
Device --> (255) <BS> (008) <SO> (014) (254) (255) <BS> (008) <FF> (012) (254)
Device --> (255) <DC2> (018) (254)
Host --> (255) <LF> (010) <NUL> (000) <STX> (002) (254)
Device --> (255) <BS> (008) <LF> (010) (254)
Host --> (255) <SO> (014) <SOH> (001) (254) (255) <VT> (011) <US> (031) (254) (255) <VT> (011) (132) (254) (255) <VT> (011) 0 (048) (254) (255) <VT> (011) <NUL> (000) (254) (255) <FF> (012) <SOH> (001) (254)
Device --> (255) <BS> (008) <SO> (014) (254) (255) <BS> (008) <VT> (011) (254) (255) <BS> (008) <VT> (011) (254)
Device --> (255) <BS> (008) <VT> (011) (254)
Device --> (255) <BS> (008) <VT> (011) (254)
Device --> (255) <BS> (008) <FF> (012) (254)
Device --> (255) <DLE> (016) (254)
Host --> (255) <SO> (014) <NUL> (000) (254) (255) <VT> (011) <DLE> (016) (254) (255) <VT> (011) G (071) (254) (255) <VT> (011) C (067) (254) (255) <VT> (011) E (069) (254) (255) <VT> (011) C (067) (254) (255) <VT> (011) T (084) (254) (255) <VT> (011) e (101) (254)
Host --> (255) <VT> (011) s (115) (254) (255) <VT> (011) t (116) (254) (255) <VT> (011) e (101) (254) (255) <FF> (012) r (114) (254)
Device --> (255) <BS> (008) <SO> (014) (254) (255) <BS> (008) <VT> (011) (254)
Device --> (255) <ENQ> (005) <SOH> (001) (254)
Device --> (255) (134) F (070) (254)
Device --> (255) <BS> (008) <VT> (011) (254)
Device --> (255) <BS> (008) <VT> (011) (254)
Device --> (255) <BS> (008) <VT> (011) (254)
Device --> (255) <BS> (008) <VT> (011) (254)
Device --> (255) <BS> (008) <VT> (011) (254)
Device --> (255) <BS> (008) <VT> (011) (254)
Device --> (255) <BS> (008) <VT> (011) (254)
Device --> (255) <BS> (008) <VT> (011) (254)
Device --> (255) <BS> (008) <VT> (011) (254)
Device --> (255) <BS> (008) <FF> (012) (254)
Device --> (255) <DLE> (016) (254)
Host --> (255) <SO> (014) <NUL> (000) (254) (255) <VT> (011) <DLE> (016) (254) (255) <FF> (012) (143) (254)
Device --> (255) <BS> (008) <SO> (014) (254) (255) <BS> (008) <VT> (011) (254) (255) <BS> (008) <FF> (012) (254)
Device --> (255) <DLE> (016) (254)
Host --> (255) <SO> (014) <NUL> (000) (254) (255) <VT> (011) <DLE> (016) (254) (255) <VT> (011) G (071) (254) (255) <VT> (011) C (067) (254) (255) <VT> (011) E (069) (254) (255) <VT> (011) C (067) (254) (255) <VT> (011) T (084) (254) (255) <VT> (011) e (101) (254)
Host --> (255) <VT> (011) s (115) (254) (255) <VT> (011) t (116) (254) (255) <VT> (011) e (101) (254) (255) <FF> (012) r (114) (254)
Device --> (255) <BS> (008) <SO> (014) (254) (255) <BS> (008) <VT> (011) (254)
Device --> (255) <BS> (008) <VT> (011) (254)
Device --> (255) <BS> (008) <VT> (011) (254)
Device --> (255) <BS> (008) <VT> (011) (254)
Device --> (255) <BS> (008) <VT> (011) (254)
Device --> (255) <BS> (008) <VT> (011) (254)
Device --> (255) <BS> (008) <VT> (011) (254)
Device --> (255) <BS> (008) <VT> (011) (254)
Device --> (255) <BS> (008) <VT> (011) (254)
Device --> (255) <BS> (008) <VT> (011) (254)
Device --> (255) <BS> (008) <FF> (012) (254)
Device --> (255) <DLE> (016) (254)
Device --> (255) <ENQ> (005) <SOH> (001) (254)
Device --> (255) (134) (140) (254)
Host --> (255) <SO> (014) <SOH> (001) (254) (255) <VT> (011) <US> (031) (254) (255) <VT> (011) (135) (254) (255) <VT> (011) <NUL> (000) (254) (255) <VT> (011) <NAK> (021) (254) (255) <FF> (012) (130) (254)
Device --> (255) <BS> (008) <SO> (014) (254) (255) <BS> (008) <VT> (011) (254)
Device --> (255) <ENQ> (005) <SOH> (001) (254)
Device --> (255) <ACK> (006) (144) (254)
Device --> (255) (134) <NUL> (000) (254)
Host --> (255) <SO> (014) <NUL> (000) (254) (255) <VT> (011) <DLE> (016) (254) (255) <FF> (012) <EOT> (004) (254)
Device --> (255) <BS> (008) <VT> (011) (254)
Device --> (255) <BS> (008) <VT> (011) (254)
Device --> (255) <BS> (008) <VT> (011) (254)
Device --> (255) <BS> (008) <FF> (012) (254)
Device --> (255) <BS> (008) <SO> (014) (254) (255) <BS> (008) <VT> (011) (254)
Device --> (255) <DLE> (016) (254)
Device --> (255) <BS> (008) <FF> (012) (254)
Device --> (255) <DLE> (016) (254)
Host --> (255) <LF> (010) <NUL> (000) <NUL> (000) (254)
Device --> (255) <BS> (008) <LF> (010) (254)
Host --> (255) <CAN> (024) <NUL> (000) (254)
Device --> (255) <BS> (008) <CAN> (024) (254)
I was disappointed to see that it wasn’t a text protocol but straight away I
noticed some patterns.
It seemed that messages start with a byte of value 255, end with 254 and that
each payload consists of only a few bytes, often just one.
In libCEC’s source code I found that the first byte of a payload is the message
code and the following bytes are the parameters.
There was also a table of message codes there.
Table of message codes
typedef enum cec_adapter_messagecode
{
MSGCODE_NOTHING = 0,
MSGCODE_PING,
MSGCODE_TIMEOUT_ERROR,
MSGCODE_HIGH_ERROR,
MSGCODE_LOW_ERROR,
MSGCODE_FRAME_START,
MSGCODE_FRAME_DATA,
MSGCODE_RECEIVE_FAILED,
MSGCODE_COMMAND_ACCEPTED,
MSGCODE_COMMAND_REJECTED,
MSGCODE_SET_ACK_MASK,
MSGCODE_TRANSMIT,
MSGCODE_TRANSMIT_EOM,
MSGCODE_TRANSMIT_IDLETIME,
MSGCODE_TRANSMIT_ACK_POLARITY,
MSGCODE_TRANSMIT_LINE_TIMEOUT,
MSGCODE_TRANSMIT_SUCCEEDED,
MSGCODE_TRANSMIT_FAILED_LINE,
MSGCODE_TRANSMIT_FAILED_ACK,
MSGCODE_TRANSMIT_FAILED_TIMEOUT_DATA,
MSGCODE_TRANSMIT_FAILED_TIMEOUT_LINE,
MSGCODE_FIRMWARE_VERSION,
MSGCODE_START_BOOTLOADER,
MSGCODE_GET_BUILDDATE,
MSGCODE_SET_CONTROLLED,
MSGCODE_GET_AUTO_ENABLED,
MSGCODE_SET_AUTO_ENABLED,
MSGCODE_GET_DEFAULT_LOGICAL_ADDRESS,
MSGCODE_SET_DEFAULT_LOGICAL_ADDRESS,
MSGCODE_GET_LOGICAL_ADDRESS_MASK,
MSGCODE_SET_LOGICAL_ADDRESS_MASK,
MSGCODE_GET_PHYSICAL_ADDRESS,
MSGCODE_SET_PHYSICAL_ADDRESS,
MSGCODE_GET_DEVICE_TYPE,
MSGCODE_SET_DEVICE_TYPE,
MSGCODE_GET_HDMI_VERSION,
MSGCODE_SET_HDMI_VERSION,
MSGCODE_GET_OSD_NAME,
MSGCODE_SET_OSD_NAME,
MSGCODE_WRITE_EEPROM,
MSGCODE_GET_ADAPTER_TYPE,
MSGCODE_SET_ACTIVE_SOURCE,
MSGCODE_GET_AUTO_POWER_ON,
MSGCODE_SET_AUTO_POWER_ON,
MSGCODE_FRAME_EOM = 0x80,
MSGCODE_FRAME_ACK = 0x40,
} cec_adapter_messagecode;
I wrote a quick Python script to decode the slsnif log using this table.
It’s available here.
And here’s the decoded log.
Decoded log
OUT PING (0x01) args
IN COMMAND_ACCEPTED (0x08) args 0x01
OUT FIRMWARE_VERSION (0x15) args
IN FIRMWARE_VERSION (0x15) args 0x00 0x0c
OUT SET_CONTROLLED (0x18) args 0x01
IN COMMAND_ACCEPTED (0x08) args 0x18
OUT GET_BUILDDATE (0x17) args
IN GET_BUILDDATE (0x17) args 0x5e 0xa8 0x3b 0xc1
OUT GET_ADAPTER_TYPE (0x28) args
IN GET_ADAPTER_TYPE (0x28) args 0x01
OUT TRANSMIT_IDLETIME (0x0d) args 0x03
IN COMMAND_ACCEPTED (0x08) args 0x0d
OUT TRANSMIT_ACK_POLARITY (0x0e) args 0x00
OUT TRANSMIT_EOM (0x0c) args 0xf0
IN COMMAND_ACCEPTED (0x08) args 0x0e
IN COMMAND_ACCEPTED (0x08) args 0x0c
IN TRANSMIT_SUCCEEDED (0x10) args
OUT TRANSMIT_ACK_POLARITY (0x0e) args 0x00
OUT TRANSMIT (0x0b) args 0xf0
OUT TRANSMIT_EOM (0x0c) args 0x8c
IN COMMAND_ACCEPTED (0x08) args 0x0e
IN COMMAND_ACCEPTED (0x08) args 0x0b
IN COMMAND_ACCEPTED (0x08) args 0x0c
IN TRANSMIT_SUCCEEDED (0x10) args
IN FRAME_START ACK (0x05) args 0x0f
IN FRAME_DATA ACK (0x06) args 0x87
IN FRAME_DATA ACK (0x06) args 0x08
IN FRAME_DATA ACK (0x06) args 0x00
IN FRAME_DATA ACK EOM (0x06) args 0x46
OUT GET_AUTO_ENABLED (0x19) args
IN GET_AUTO_ENABLED (0x19) args 0x01
OUT GET_DEFAULT_LOGICAL_ADDRESS (0x1b) args
IN GET_DEFAULT_LOGICAL_ADDRESS (0x1b) args 0x01
OUT GET_DEVICE_TYPE (0x21) args
IN GET_DEVICE_TYPE (0x21) args 0x01
OUT GET_LOGICAL_ADDRESS_MASK (0x1d) args
IN GET_LOGICAL_ADDRESS_MASK (0x1d) args 0x02 0x06
OUT GET_OSD_NAME (0x25) args
IN GET_OSD_NAME (0x25) args 0x43 0x45 0x43 0x54 0x65 0x73 0x74 0x65 0x72
OUT GET_PHYSICAL_ADDRESS (0x1f) args
IN GET_PHYSICAL_ADDRESS (0x1f) args 0x30 0x00
OUT GET_AUTO_POWER_ON (0x2a) args
IN GET_AUTO_POWER_ON (0x2a) args 0x00
OUT TRANSMIT_ACK_POLARITY (0x0e) args 0x00
OUT TRANSMIT_EOM (0x0c) args 0x11
IN COMMAND_ACCEPTED (0x08) args 0x0e
IN COMMAND_ACCEPTED (0x08) args 0x0c
IN TRANSMIT_FAILED_ACK (0x12) args
OUT TRANSMIT_ACK_POLARITY (0x0e) args 0x00
OUT TRANSMIT_EOM (0x0c) args 0x11
IN COMMAND_ACCEPTED (0x08) args 0x0e
IN COMMAND_ACCEPTED (0x08) args 0x0c
IN TRANSMIT_FAILED_ACK (0x12) args
OUT SET_ACK_MASK (0x0a) args 0x00 0x02
IN COMMAND_ACCEPTED (0x08) args 0x0a
OUT TRANSMIT_ACK_POLARITY (0x0e) args 0x01
OUT TRANSMIT (0x0b) args 0x1f
OUT TRANSMIT (0x0b) args 0x84
OUT TRANSMIT (0x0b) args 0x30
OUT TRANSMIT (0x0b) args 0x00
OUT TRANSMIT_EOM (0x0c) args 0x01
IN COMMAND_ACCEPTED (0x08) args 0x0e
IN COMMAND_ACCEPTED (0x08) args 0x0b
IN COMMAND_ACCEPTED (0x08) args 0x0b
IN COMMAND_ACCEPTED (0x08) args 0x0b
IN COMMAND_ACCEPTED (0x08) args 0x0b
IN COMMAND_ACCEPTED (0x08) args 0x0c
IN TRANSMIT_SUCCEEDED (0x10) args
OUT TRANSMIT_ACK_POLARITY (0x0e) args 0x00
OUT TRANSMIT (0x0b) args 0x10
OUT TRANSMIT (0x0b) args 0x47
OUT TRANSMIT (0x0b) args 0x43
OUT TRANSMIT (0x0b) args 0x45
OUT TRANSMIT (0x0b) args 0x43
OUT TRANSMIT (0x0b) args 0x54
OUT TRANSMIT (0x0b) args 0x65
OUT TRANSMIT (0x0b) args 0x73
OUT TRANSMIT (0x0b) args 0x74
OUT TRANSMIT (0x0b) args 0x65
OUT TRANSMIT_EOM (0x0c) args 0x72
IN COMMAND_ACCEPTED (0x08) args 0x0e
IN COMMAND_ACCEPTED (0x08) args 0x0b
IN FRAME_START (0x05) args 0x01
IN FRAME_DATA EOM (0x06) args 0x46
IN COMMAND_ACCEPTED (0x08) args 0x0b
IN COMMAND_ACCEPTED (0x08) args 0x0b
IN COMMAND_ACCEPTED (0x08) args 0x0b
IN COMMAND_ACCEPTED (0x08) args 0x0b
IN COMMAND_ACCEPTED (0x08) args 0x0b
IN COMMAND_ACCEPTED (0x08) args 0x0b
IN COMMAND_ACCEPTED (0x08) args 0x0b
IN COMMAND_ACCEPTED (0x08) args 0x0b
IN COMMAND_ACCEPTED (0x08) args 0x0b
IN COMMAND_ACCEPTED (0x08) args 0x0c
IN TRANSMIT_SUCCEEDED (0x10) args
OUT TRANSMIT_ACK_POLARITY (0x0e) args 0x00
OUT TRANSMIT (0x0b) args 0x10
OUT TRANSMIT_EOM (0x0c) args 0x8f
IN COMMAND_ACCEPTED (0x08) args 0x0e
IN COMMAND_ACCEPTED (0x08) args 0x0b
IN COMMAND_ACCEPTED (0x08) args 0x0c
IN TRANSMIT_SUCCEEDED (0x10) args
OUT TRANSMIT_ACK_POLARITY (0x0e) args 0x00
OUT TRANSMIT (0x0b) args 0x10
OUT TRANSMIT (0x0b) args 0x47
OUT TRANSMIT (0x0b) args 0x43
OUT TRANSMIT (0x0b) args 0x45
OUT TRANSMIT (0x0b) args 0x43
OUT TRANSMIT (0x0b) args 0x54
OUT TRANSMIT (0x0b) args 0x65
OUT TRANSMIT (0x0b) args 0x73
OUT TRANSMIT (0x0b) args 0x74
OUT TRANSMIT (0x0b) args 0x65
OUT TRANSMIT_EOM (0x0c) args 0x72
IN COMMAND_ACCEPTED (0x08) args 0x0e
IN COMMAND_ACCEPTED (0x08) args 0x0b
IN COMMAND_ACCEPTED (0x08) args 0x0b
IN COMMAND_ACCEPTED (0x08) args 0x0b
IN COMMAND_ACCEPTED (0x08) args 0x0b
IN COMMAND_ACCEPTED (0x08) args 0x0b
IN COMMAND_ACCEPTED (0x08) args 0x0b
IN COMMAND_ACCEPTED (0x08) args 0x0b
IN COMMAND_ACCEPTED (0x08) args 0x0b
IN COMMAND_ACCEPTED (0x08) args 0x0b
IN COMMAND_ACCEPTED (0x08) args 0x0b
IN COMMAND_ACCEPTED (0x08) args 0x0c
IN TRANSMIT_SUCCEEDED (0x10) args
IN FRAME_START (0x05) args 0x01
IN FRAME_DATA EOM (0x06) args 0x8c
OUT TRANSMIT_ACK_POLARITY (0x0e) args 0x01
OUT TRANSMIT (0x0b) args 0x1f
OUT TRANSMIT (0x0b) args 0x87
OUT TRANSMIT (0x0b) args 0x00
OUT TRANSMIT (0x0b) args 0x15
OUT TRANSMIT_EOM (0x0c) args 0x82
IN COMMAND_ACCEPTED (0x08) args 0x0e
IN COMMAND_ACCEPTED (0x08) args 0x0b
IN FRAME_START (0x05) args 0x01
IN FRAME_DATA (0x06) args 0x90
IN FRAME_DATA EOM (0x06) args 0x00
OUT TRANSMIT_ACK_POLARITY (0x0e) args 0x00
OUT TRANSMIT (0x0b) args 0x10
OUT TRANSMIT_EOM (0x0c) args 0x04
IN COMMAND_ACCEPTED (0x08) args 0x0b
IN COMMAND_ACCEPTED (0x08) args 0x0b
IN COMMAND_ACCEPTED (0x08) args 0x0b
IN COMMAND_ACCEPTED (0x08) args 0x0c
IN COMMAND_ACCEPTED (0x08) args 0x0e
IN COMMAND_ACCEPTED (0x08) args 0x0b
IN TRANSMIT_SUCCEEDED (0x10) args
IN COMMAND_ACCEPTED (0x08) args 0x0c
IN TRANSMIT_SUCCEEDED (0x10) args
OUT SET_ACK_MASK (0x0a) args 0x00 0x00
IN COMMAND_ACCEPTED (0x08) args 0x0a
OUT SET_CONTROLLED (0x18) args 0x00
IN COMMAND_ACCEPTED (0x08) args 0x18
I found out that the TRANSMIT
messages cause actual CEC transmissions.
Now I had to pinpoint the ones that turn the TV on.
I compared the decoded log with the cec-client debug log and found the
correct lines.
OUT TRANSMIT_ACK_POLARITY (0x0e) args 0x00
OUT TRANSMIT (0x0b) args 0x10
OUT TRANSMIT_EOM (0x0c) args 0x04
IN COMMAND_ACCEPTED (0x08) args 0x0e
IN COMMAND_ACCEPTED (0x08) args 0x0b
IN COMMAND_ACCEPTED (0x08) args 0x0c
IN TRANSMIT_SUCCEEDED (0x10) args
NOTICE: [ 2112] << powering on 'TV' (0)
TRAFFIC: [ 2112] << 10:04
It seemed that sending 10:04 to the CEC bus causes the TV to turn on.
And indeed, I pasted this frame into CEC-O-MATIC CEC decoder and got:
- source: Recording 1
- destination: TV
- End-user features > One Touch Play > Image View On
From the description it looked to me like a correct message that would turn on
the TV.
I sent these messages to the CEC adapter with Python and pySerial but
after sending the first command, it was immediately rejected
(9 = COMMAND_REJECTED).
$ python3
Python 3.11.4 (main, Jun 9 2023, 02:29:05) [GCC 12.2.1 20220924] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import serial
>>> ser = serial.Serial("/dev/ttyACM0", 38400, timeout=2)
>>> ser.write(b"\xff\x0e\x00\xfe")
4
>>> list(ser.read_until(b"\xfe"))
[255, 9, 14, 254]
I figured I had to send more than just the transmission messages.
After a quick glance at the decoded log, SET_CONTROLLED
drew my attention.
I sent a PING
, then SET_CONTROLLED 1
, then TRANSMIT_ACK_POLARITY
,
TRANSMIT
, TRANSMIT_EOM
and finally SET_CONTROLLED 0
.
That did the trick.
My TV turned on.
Now what was left to do was to find out the correct standby frame by repeating
these steps for the cec-client’s standby command (it turned out to be 10:36),
write a tool I could use to control the power state of my TV, measure how much
time it takes to do it and hope it’s significantly faster than cec-client.
I gathered all the information I had and wrote a Python script that could
set the TV to standby mode or turn it on.
I named it Fast CEC and you can find it here.
Let’s see how fast it is.
~ $ time fastcec /dev/ttyACM0 on
real 0m 0.09s
user 0m 0.01s
sys 0m 0.00s
~ $ time fastcec /dev/ttyACM0 standby
real 0m 0.09s
user 0m 0.02s
sys 0m 0.00s
Fast CEC at 0.09 s is 37 times faster than cec-client at 3.31 s.
I consider this endevour a success not only because of the significant speedup
so that my TV turns on a bit faster.
I also really enjoyed learning about CEC, analyzing libCEC and the adapter
protocol and putting this all together into a useful tool.