diff --git a/decoders/cec/__init__.py b/decoders/cec/__init__.py
new file mode 100644
index 0000000..4dc2d18
--- /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 <jorgeso...@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..c9adf4d
--- /dev/null
+++ b/decoders/cec/pd.py
@@ -0,0 +1,354 @@
+##
+## This file is part of the libsigrokdecode project.
+##
+## Copyright (C) 2018 Jorge Solla Rubiales <jorgeso...@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 Consumer Electronics Control protocol'
+    desc = ("Consumer Electronics Control (CEC) protocol allows users "
+            "to command and control devices connected through HDMI"
+            )
+    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):
+            # Parse header
+            if i == 0:
+                (src, dst) = decode_header(self.cmd_bytes[i]['val'])
+                str = "HDR: " + src + ", " + dst
+            # Parse opcode
+            elif i == 1:
+                str += " | OPC: " + decode_opcode(self.cmd_bytes[i]['val'])
+            # Parse operands
+            else:
+                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 (acknowledgement 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 fall 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..7f4b540
--- /dev/null
+++ b/decoders/cec/protocoldata.py
@@ -0,0 +1,129 @@
+##
+## This file is part of the libsigrokdecode project.
+##
+## Copyright (C) 2018 Jorge Solla Rubiales <jorgeso...@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'
------------------------------------------------------------------------------
Check out the vibrant tech community on one of the world's most
engaging tech sites, Slashdot.org! http://sdm.link/slashdot
_______________________________________________
sigrok-devel mailing list
sigrok-devel@lists.sourceforge.net
https://lists.sourceforge.net/lists/listinfo/sigrok-devel

Reply via email to