Reverse engineering the Pulse-Eight CEC adapter 19 July 2023 on Krystian's Keep

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.

A small black electronic device with a USB and HDMI port lying on a wooden table.

Pulse-Eight USB - CEC Adapter

The same device, top view. The ports are marked: DATA, TV, PC.

Pulse-Eight USB - CEC Adapter (top view)

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 ready1 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:

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.

New tool

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.


  1. I noticed that disconnecting two hard drives reduces PC resume time from 8 to 4.5 seconds. I might investigate this in the future. ↩︎

Have a comment on one of my posts? Start a discussion in my public inbox by sending an email to ~krystianch/public-inbox@lists.sr.ht [mailing list etiquette]

Articles from blogs I read

Neurodivergence and accountability in free software

In November of last year, I wrote Richard Stallman’s political discourse on sex, which argues that Richard Stallman, the founder of and present-day voting member of the board of directors of the Free Software Foundation (FSF), endorses and advocates for a ha…

via Drew DeVault's blog September 25, 2024

Status update, September 2024

Hi! Once again, this status update will be rather short due to limited time bandwidth. I hope to be able to allocate a bit more time slots for my open-source projects next month. We’re getting closer to a new Sway release (fingers crossed), with lots of help f…

via emersion September 20, 2024

What's cooking on SourceHut? September 2024

Hello everyone! It has been some time since we last wrote a “What’s cooking” for you. We’d like to resume this tradition as of this September. We haven’t been totally radio silent – you can get caught up on what’s been happening over these past two years rea…

via Blogs on Sourcehut September 16, 2024

Generated by openring