summaryrefslogtreecommitdiff
path: root/decoders/cec
diff options
context:
space:
mode:
authorJorge Solla <jorgesolla@gmail.com>2018-09-02 22:01:33 +0200
committerUwe Hermann <uwe@hermann-uwe.de>2018-09-09 17:39:01 +0200
commitfb248c04c3a96c90aab6472d8641683281f46f69 (patch)
tree0db1df8dde3d5264aad0d0f9ab8220f775584ae1 /decoders/cec
parent43047b89962667436f6ed11c5da5e6f4a1ccfbde (diff)
downloadlibsigrokdecode-fb248c04c3a96c90aab6472d8641683281f46f69.tar.gz
libsigrokdecode-fb248c04c3a96c90aab6472d8641683281f46f69.zip
Add HDMI CEC protocol decoder.
Diffstat (limited to 'decoders/cec')
-rw-r--r--decoders/cec/__init__.py25
-rw-r--r--decoders/cec/pd.py322
-rw-r--r--decoders/cec/protocoldata.py126
3 files changed, 473 insertions, 0 deletions
diff --git a/decoders/cec/__init__.py b/decoders/cec/__init__.py
new file mode 100644
index 0000000..db288ab
--- /dev/null
+++ b/decoders/cec/__init__.py
@@ -0,0 +1,25 @@
+##
+## This file is part of the libsigrokdecode project.
+##
+## Copyright (C) 2018 Jorge Solla Rubiales <jorgesolla@gmail.com>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, see <http://www.gnu.org/licenses/>.
+##
+
+'''
+Consumer Electronics Control (CEC) protocol allows users to command and
+control devices connected through HDMI.
+'''
+
+from .pd import Decoder
diff --git a/decoders/cec/pd.py b/decoders/cec/pd.py
new file mode 100644
index 0000000..6c84a4b
--- /dev/null
+++ b/decoders/cec/pd.py
@@ -0,0 +1,322 @@
+##
+## This file is part of the libsigrokdecode project.
+##
+## Copyright (C) 2018 Jorge Solla Rubiales <jorgesolla@gmail.com>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program. If not, see <http://www.gnu.org/licenses/>.
+##
+
+import sigrokdecode as srd
+from .protocoldata import *
+
+# Pulse types
+class Pulse:
+ INVALID, START, ZERO, ONE = range(4)
+
+# Protocol stats
+class Stat:
+ WAIT_START, GET_BITS, WAIT_EOM, WAIT_ACK = range(4)
+
+# Pulse times in milliseconds
+timing = {
+ Pulse.START: {
+ 'low': { 'min': 3.5, 'max': 3.9 },
+ 'total': { 'min': 4.3, 'max': 4.7 }
+ },
+ Pulse.ZERO: {
+ 'low': { 'min': 1.3, 'max': 1.7 },
+ 'total': { 'min': 2.05, 'max': 2.75 }
+ },
+ Pulse.ONE: {
+ 'low': { 'min': 0.4, 'max': 0.8 },
+ 'total': { 'min': 2.05, 'max': 2.75 }
+ }
+}
+
+class ChannelError(Exception):
+ pass
+
+class Decoder(srd.Decoder):
+ api_version = 3
+ id = 'cec'
+ name = 'CEC'
+ longname = 'HDMI-CEC'
+ desc = 'HDMI Consumer Electronics Control (CEC) protocol.'
+ license = 'gplv2+'
+ inputs = ['logic']
+ outputs = ['cec']
+ channels = (
+ {'id': 'cec', 'name': 'CEC', 'desc': 'CEC bus data'},
+ )
+ annotations = (
+ ('st', 'Start'),
+ ('eom-0', 'End of message'),
+ ('eom-1', 'Message continued'),
+ ('nack', 'ACK not set'),
+ ('ack', 'ACK set'),
+ ('bits', 'Bits'),
+ ('bytes', 'Bytes'),
+ ('frames', 'Frames'),
+ ('sections', 'Sections'),
+ ('warnings', 'Warnings')
+ )
+ annotation_rows = (
+ ('bits', 'Bits', (0, 1, 2, 3, 4, 5)),
+ ('bytes', 'Bytes', (6,)),
+ ('frames', 'Frames', (7,)),
+ ('sections', 'Sections', (8,)),
+ ('warnings', 'Warnings', (9,))
+ )
+
+ def __init__(self):
+ self.reset()
+
+ def precalculate(self):
+ # Restrict max length of ACK/NACK labels to 2 BIT pulses.
+ bit_time = timing[Pulse.ZERO]['total']['min']
+ bit_time = bit_time * 2
+ self.max_ack_len_samples = round((bit_time / 1000) * self.samplerate)
+
+ def reset(self):
+ self.stat = Stat.WAIT_START
+ self.samplerate = None
+ self.fall_start = None
+ self.fall_end = None
+ self.rise = None
+ self.reset_frame_vars()
+
+ def reset_frame_vars(self):
+ self.eom = None
+ self.bit_count = 0
+ self.byte_count = 0
+ self.byte = 0
+ self.byte_start = None
+ self.frame_start = None
+ self.frame_end = None
+ self.is_nack = 0
+ self.cmd_bytes = []
+
+ def metadata(self, key, value):
+ if key == srd.SRD_CONF_SAMPLERATE:
+ self.samplerate = value
+ self.precalculate()
+
+ def set_stat(self, stat):
+ self.stat = stat
+
+ def handle_frame(self, is_nack):
+ if self.fall_start is None or self.fall_end is None:
+ return
+
+ i = 0
+ str = ''
+ while i < len(self.cmd_bytes):
+ str += '{:02x}'.format(self.cmd_bytes[i]['val'])
+ if i != (len(self.cmd_bytes) - 1):
+ str += ':'
+ i += 1
+
+ self.put(self.frame_start, self.frame_end, self.out_ann, [7, [str]])
+
+ i = 0
+ operands = 0
+ str = ''
+ while i < len(self.cmd_bytes):
+ if i == 0: # Parse header
+ (src, dst) = decode_header(self.cmd_bytes[i]['val'])
+ str = 'HDR: ' + src + ', ' + dst
+ elif i == 1: # Parse opcode
+ str += ' | OPC: ' + decode_opcode(self.cmd_bytes[i]['val'])
+ else: # Parse operands
+ if operands == 0:
+ str += ' | OPS: '
+ operands += 1
+ str += '0x{:02x}'.format(self.cmd_bytes[i]['val'])
+ if i != len(self.cmd_bytes) - 1:
+ str += ', '
+ i += 1
+
+ # Header only commands are PINGS
+ if i == 1:
+ if self.eom:
+ str += ' | OPC: PING'
+ else:
+ str += ' | OPC: NONE. Aborted cmd'
+
+ # Add extra information (ack of the command from the destination)
+ if is_nack:
+ str += ' | R: NACK'
+ else:
+ str += ' | R: ACK'
+
+ self.put(self.frame_start, self.frame_end, self.out_ann, [8, [str]])
+
+ def process(self):
+ zero_time = ((self.rise - self.fall_start) / self.samplerate) * 1000.0
+ total_time = ((self.fall_end - self.fall_start) / self.samplerate) * 1000.0
+ pulse = Pulse.INVALID
+
+ # VALIDATION: Identify pulse based on length of the low period
+ for key in timing:
+ if zero_time >= timing[key]['low']['min'] and zero_time <= timing[key]['low']['max']:
+ pulse = key
+ break
+
+ # VALIDATION: Invalid pulse
+ if pulse == Pulse.INVALID:
+ self.set_stat(Stat.WAIT_START)
+ self.put(self.fall_start, self.fall_end, self.out_ann, [9, ['Invalid pulse: Wrong timing']])
+ return
+
+ # VALIDATION: If waiting for start, discard everything else
+ if self.stat == Stat.WAIT_START and pulse != Pulse.START:
+ self.put(self.fall_start, self.fall_end, self.out_ann, [9, ['Expected START: BIT found']])
+ return
+
+ # VALIDATION: If waiting for ACK or EOM, only BIT pulses (0/1) are expected
+ if (self.stat == Stat.WAIT_ACK or self.stat == Stat.WAIT_EOM) and pulse == Pulse.START:
+ self.put(self.fall_start, self.fall_end, self.out_ann, [9, ['Expected BIT: START received)']])
+ self.set_stat(Stat.WAIT_START)
+
+ # VALIDATION: ACK bit pulse remains high till the next frame (if any): Validate only min time of the low period
+ if self.stat == Stat.WAIT_ACK and pulse != Pulse.START:
+ if total_time < timing[pulse]['total']['min']:
+ pulse = Pulse.INVALID
+ self.put(self.fall_start, self.fall_end, self.out_ann, [9, ['ACK pulse below minimun time']])
+ self.set_stat(Stat.WAIT_START)
+ return
+
+ # VALIDATION / PING FRAME DETECTION: Initiator doesn't sets the EOM = 1 but stops sending when ack doesn't arrive
+ if self.stat == Stat.GET_BITS and pulse == Pulse.START:
+ # Make sure we received a complete byte to consider it a valid ping
+ if self.bit_count == 0:
+ self.handle_frame(self.is_nack)
+ else:
+ self.put(self.frame_start, self.samplenum, self.out_ann, [9, ['ERROR: Incomplete byte received']])
+
+ # Set wait start so we receive next frame
+ self.set_stat(Stat.WAIT_START)
+
+ # VALIDATION: Check timing of the BIT (0/1) pulse in any other case (not waiting for ACK)
+ if self.stat != Stat.WAIT_ACK and pulse != Pulse.START:
+ if total_time < timing[pulse]['total']['min'] or total_time > timing[pulse]['total']['max']:
+ self.put(self.fall_start, self.fall_end, self.out_ann, [9, ['Bit pulse exceeds total pulse timespan']])
+ pulse = Pulse.INVALID
+ self.set_stat(Stat.WAIT_START)
+ return
+
+ if pulse == Pulse.ZERO:
+ bit = 0
+ elif pulse == Pulse.ONE:
+ bit = 1
+
+ # STATE: WAIT START
+ if self.stat == Stat.WAIT_START:
+ self.set_stat(Stat.GET_BITS)
+ self.reset_frame_vars()
+ self.put(self.fall_start, self.fall_end, self.out_ann, [0, ['ST']])
+
+ # STATE: GET BITS
+ elif self.stat == Stat.GET_BITS:
+ # Reset stats on first bit
+ if self.bit_count == 0:
+ self.byte_start = self.fall_start
+ self.byte = 0
+
+ # If 1st byte of the datagram save its sample num
+ if len(self.cmd_bytes) == 0:
+ self.frame_start = self.fall_start
+
+ self.byte += (bit << (7 - self.bit_count))
+ self.bit_count += 1
+ self.put(self.fall_start, self.fall_end, self.out_ann, [5, [str(bit)]])
+
+ if self.bit_count == 8:
+ self.bit_count = 0
+ self.byte_count += 1
+ self.set_stat(Stat.WAIT_EOM)
+ self.put(self.byte_start, self.samplenum, self.out_ann, [6, ['0x{:02x}'.format(self.byte)]])
+ self.cmd_bytes.append({'st': self.byte_start, 'ed': self.samplenum, 'val': self.byte})
+
+ # STATE: WAIT EOM
+ elif self.stat == Stat.WAIT_EOM:
+ self.eom = bit
+ self.frame_end = self.fall_end
+
+ if self.eom:
+ self.put(self.fall_start, self.fall_end, self.out_ann, [2, ['EOM=Y']])
+ else:
+ self.put(self.fall_start, self.fall_end, self.out_ann, [1, ['EOM=N']])
+
+ self.set_stat(Stat.WAIT_ACK)
+
+ # STATE: WAIT ACK
+ elif self.stat == Stat.WAIT_ACK:
+ # If a frame with broadcast destination is being sent, the ACK is
+ # inverted: a 0 is considered a NACK, therefore we invert the value
+ # of the bit here, so we match the real meaning of it.
+ if (self.cmd_bytes[0]['val'] & 0x0F) == 0x0F:
+ bit = ~bit & 0x01
+
+ if (self.fall_end - self.fall_start) > self.max_ack_len_samples:
+ ann_end = self.fall_start + self.max_ack_len_samples
+ else:
+ ann_end = self.fall_end
+
+ if bit:
+ # Any NACK detected in the frame is enough to consider the
+ # whole frame NACK'd.
+ self.is_nack = 1
+ self.put(self.fall_start, ann_end, self.out_ann, [3, ['NACK']])
+ else:
+ self.put(self.fall_start, ann_end, self.out_ann, [4, ['ACK']])
+
+ # After ACK bit, wait for new datagram or continue reading current
+ # one based on EOM value.
+ if self.eom or self.is_nack:
+ self.set_stat(Stat.WAIT_START)
+ self.handle_frame(self.is_nack)
+ else:
+ self.set_stat(Stat.GET_BITS)
+
+ def start(self):
+ self.out_ann = self.register(srd.OUTPUT_ANN)
+
+ def decode(self):
+ if not self.samplerate:
+ raise SamplerateError('Cannot decode without samplerate.')
+
+ # Wait for first falling edge.
+ self.wait({0: 'f'})
+ self.fall_end = self.samplenum
+
+ while True:
+ self.wait({0: 'r'})
+ self.rise = self.samplenum
+
+ if self.stat == Stat.WAIT_ACK:
+ self.wait([{0: 'f'}, {'skip': self.max_ack_len_samples}])
+ else:
+ self.wait([{0: 'f'}])
+
+ self.fall_start = self.fall_end
+ self.fall_end = self.samplenum
+ self.process()
+
+ # If there was a timeout while waiting for ACK: RESYNC.
+ # Note: This is an expected situation as no new falling edge will
+ # happen until next frame is transmitted.
+ if self.matched == (False, True):
+ self.wait({0: 'f'})
+ self.fall_end = self.samplenum
diff --git a/decoders/cec/protocoldata.py b/decoders/cec/protocoldata.py
new file mode 100644
index 0000000..dd867e0
--- /dev/null
+++ b/decoders/cec/protocoldata.py
@@ -0,0 +1,126 @@
+##
+## This file is part of the libsigrokdecode project.
+##
+## Copyright (C) 2018 Jorge Solla Rubiales <jorgesolla@gmail.com>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program. If not, see <http://www.gnu.org/licenses/>.
+##
+
+logical_adresses = [
+ 'TV',
+ 'Recording_1',
+ 'Recording_2',
+ 'Tuner_1',
+ 'Playback_1',
+ 'AudioSystem',
+ 'Tuner2',
+ 'Tuner3',
+ 'Playback_2',
+ 'Recording_3',
+ 'Tuner_4',
+ 'Playback_3',
+ 'Backup_1',
+ 'Backup_2',
+ 'FreeUse',
+]
+
+# List taken from LibCEC.
+opcodes = {
+ 0x82: 'ACTIVE_SOURCE',
+ 0x04: 'IMAGE_VIEW_ON',
+ 0x0D: 'TEXT_VIEW_ON',
+ 0x9D: 'INACTIVE_SOURCE',
+ 0x85: 'REQUEST_ACTIVE_SOURCE',
+ 0x80: 'ROUTING_CHANGE',
+ 0x81: 'ROUTING_INFORMATION',
+ 0x86: 'SET_STREAM_PATH',
+ 0x36: 'STANDBY',
+ 0x0B: 'RECORD_OFF',
+ 0x09: 'RECORD_ON',
+ 0x0A: 'RECORD_STATUS',
+ 0x0F: 'RECORD_TV_SCREEN',
+ 0x33: 'CLEAR_ANALOGUE_TIMER',
+ 0x99: 'CLEAR_DIGITAL_TIMER',
+ 0xA1: 'CLEAR_EXTERNAL_TIMER',
+ 0x34: 'SET_ANALOGUE_TIMER',
+ 0x97: 'SET_DIGITAL_TIMER',
+ 0xA2: 'SET_EXTERNAL_TIMER',
+ 0x67: 'SET_TIMER_PROGRAM_TITLE',
+ 0x43: 'TIMER_CLEARED_STATUS',
+ 0x35: 'TIMER_STATUS',
+ 0x9E: 'CEC_VERSION',
+ 0x9F: 'GET_CEC_VERSION',
+ 0x83: 'GIVE_PHYSICAL_ADDRESS',
+ 0x91: 'GET_MENU_LANGUAGE',
+ 0x84: 'REPORT_PHYSICAL_ADDRESS',
+ 0x32: 'SET_MENU_LANGUAGE',
+ 0x42: 'DECK_CONTROL',
+ 0x1B: 'DECK_STATUS',
+ 0x1A: 'GIVE_DECK_STATUS',
+ 0x41: 'PLAY',
+ 0x08: 'GIVE_TUNER_DEVICE_STATUS',
+ 0x92: 'SELECT_ANALOGUE_SERVICE',
+ 0x93: 'SELECT_DIGITAL_SERVICE',
+ 0x07: 'TUNER_DEVICE_STATUS',
+ 0x06: 'TUNER_STEP_DECREMENT',
+ 0x05: 'TUNER_STEP_INCREMENT',
+ 0x87: 'DEVICE_VENDOR_ID',
+ 0x8C: 'GIVE_DEVICE_VENDOR_ID',
+ 0x89: 'VENDOR_COMMAND',
+ 0xA0: 'VENDOR_COMMAND_WITH_ID',
+ 0x8A: 'VENDOR_REMOTE_BUTTON_DOWN',
+ 0x8B: 'VENDOR_REMOTE_BUTTON_UP',
+ 0x64: 'SET_OSD_STRING',
+ 0x46: 'GIVE_OSD_NAME',
+ 0x47: 'SET_OSD_NAME',
+ 0x8D: 'MENU_REQUEST',
+ 0x8E: 'MENU_STATUS',
+ 0x44: 'USER_CONTROL_PRESSED',
+ 0x45: 'USER_CONTROL_RELEASE',
+ 0x8F: 'GIVE_DEVICE_POWER_STATUS',
+ 0x90: 'REPORT_POWER_STATUS',
+ 0x00: 'FEATURE_ABORT',
+ 0xFF: 'ABORT',
+ 0x71: 'GIVE_AUDIO_STATUS',
+ 0x7D: 'GIVE_SYSTEM_AUDIO_MODE_STATUS',
+ 0x7A: 'REPORT_AUDIO_STATUS',
+ 0x72: 'SET_SYSTEM_AUDIO_MODE',
+ 0x70: 'SYSTEM_AUDIO_MODE_REQUEST',
+ 0x7E: 'SYSTEM_AUDIO_MODE_STATUS',
+ 0x9A: 'SET_AUDIO_RATE',
+}
+
+def resolve_logical_address(id, is_initiator):
+ if id < 0 or id > 0x0F:
+ return 'Invalid'
+
+ # Special handling of 0x0F.
+ if id == 0x0F:
+ if is_initiator:
+ return 'Unregistered'
+ else:
+ return 'Broadcast'
+
+ return logical_adresses[id]
+
+def decode_header(header):
+ src = (header & 0xF0) >> 4
+ dst = (header & 0x0F)
+ return (resolve_logical_address(src, 1), resolve_logical_address(dst, 0))
+
+def decode_opcode(opcode):
+ if opcode in opcodes:
+ return opcodes[opcode]
+ else:
+ return 'Invalid'