]> sigrok.org Git - libsigrokdecode.git/blobdiff - decoders/dmx512/pd.py
avr_isp: Add more parts
[libsigrokdecode.git] / decoders / dmx512 / pd.py
index 3fd2aba0d5668be7f76c43f52c6681cd89782416..a0cd83f3db97a706ea8ededa485168f914b50064 100644 (file)
@@ -2,6 +2,7 @@
 ## This file is part of the libsigrokdecode project.
 ##
 ## Copyright (C) 2016 Fabian J. Stumpf <sigrok@fabianstumpf.de>
+## Copyright (C) 2019-2020 Gerhard Sittig <gerhard.sittig@gmx.net>
 ##
 ## 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
 ## along with this program; if not, see <http://www.gnu.org/licenses/>.
 ##
 
+'''
+OUTPUT_PYTHON format:
+
+Packet:
+[<ptype>, <pdata>]
+
+This is the list of <ptype> codes and their respective <pdata> values:
+ - 'PACKET': The data is a list of tuples with the bytes' start and end
+   positions as well as a byte value and a validity flag. This output
+   represents a DMX packet. The sample numbers span the range beginning
+   at the start of the start code and ending at the end of the last data
+   byte in the packet. The start code value resides at index 0.
+
+Developer notes on the DMX512 protocol:
+
+See Wikipedia for an overview:
+  https://en.wikipedia.org/wiki/DMX512#Electrical (physics, transport)
+  https://en.wikipedia.org/wiki/DMX512#Protocol (UART frames, DMX frames)
+  RS-485 transport, differential thus either polarity (needs user spec)
+  8n2 UART frames at 250kbps, BREAK to start a new DMX frame
+  slot 0 carries start code, slot 1 up to 512 max carry data for peripherals
+  start code 0 for "boring lights", non-zero start code for extensions.
+
+TODO
+- Cover more DMX packet types beyond start code 0x00 (standard). See
+  https://en.wikipedia.org/wiki/DMX512#Protocol for a list (0x17 text,
+  0xcc RDM, 0xcf sysinfo) and a reference to the ESTA database. These
+  can either get added here or can get implemented in a stacked decoder.
+- Run on more captures as these become available. Verify the min/max
+  BREAK, MARK, and RESET to RESET period checks. Add more conditions that
+  are worth checking to determine the health of the bus, see the (German)
+  http://www.soundlight.de/techtips/dmx512/dmx2000a.htm article for ideas.
+- Is there a more user friendly way of having the DMX512 decoder configure
+  the UART decoder's parameters? Currently users need to setup the polarity
+  (which is acceptable, and an essential feature), but also the bitrate and
+  frame format (which may or may not be considered acceptable).
+- (Not a DMX512 decoder TODO item) Current UART decoder implementation does
+  not handle two STOP bits, but DMX512 will transparently benefit when UART
+  gets adjusted. Until then the second STOP bit will be mistaken for a MARK
+  but that's just cosmetics, available data gets interpreted correctly.
+'''
+
 import sigrokdecode as srd
 
+class Ann:
+    BREAK, MAB, INTERFRAME, INTERPACKET, STARTCODE, DATABYTE, CHANNEL_DATA, \
+    SLOT_DATA, RESET, WARN, ERROR = range(11)
+
 class Decoder(srd.Decoder):
     api_version = 3
     id = 'dmx512'
@@ -26,30 +73,49 @@ class Decoder(srd.Decoder):
     longname = 'Digital MultipleX 512'
     desc = 'Digital MultipleX 512 (DMX512) lighting protocol.'
     license = 'gplv2+'
-    inputs = ['logic']
-    outputs = []
+    inputs = ['uart']
+    outputs = ['dmx512']
     tags = ['Embedded/industrial', 'Lighting']
-    channels = (
-        {'id': 'dmx', 'name': 'DMX data', 'desc': 'Any DMX data line'},
+    options = (
+        {'id': 'min_break', 'desc': 'Minimum BREAK length (us)', 'default': 88},
+        {'id': 'max_mark', 'desc': 'Maximum MARK length (us)', 'default': 1000000},
+        {'id': 'min_break_break', 'desc': 'Minimum BREAK to BREAK interval (us)',
+            'default': 1196},
+        {'id': 'max_reset_reset', 'desc': 'Maximum RESET to RESET interval (us)',
+         'default': 1250000},
+        {'id': 'show_zero', 'desc': 'Display all-zero set-point values',
+            'default': 'no', 'values': ('yes', 'no')},
+        {'id': 'format', 'desc': 'Data format', 'default': 'dec',
+            'values': ('dec', 'hex', 'bin')},
     )
     annotations = (
-        ('bit', 'Bit'),
+        # Lowest layer (above UART): BREAK MARK ( FRAME [MARK] )*
+        # with MARK being after-break or inter-frame or inter-packet.
         ('break', 'Break'),
         ('mab', 'Mark after break'),
-        ('startbit', 'Start bit'),
-        ('stopbits', 'Stop bit'),
-        ('startcode', 'Start code'),
-        ('channel', 'Channel'),
         ('interframe', 'Interframe'),
         ('interpacket', 'Interpacket'),
-        ('data', 'Data'),
+        # Next layer: STARTCODE ( DATABYTE )*
+        ('startcode', 'Start code'),
+        ('databyte', 'Data byte'),
+        # Next layer: CHANNEL or SLOT values
+        ('chan_data', 'Channel data'),
+        ('slot_data', 'Slot data'),
+        # Next layer: RESET
+        ('reset', 'Reset sequence'),
+        # Warnings and errors.
+        ('warning', 'Warning'),
         ('error', 'Error'),
     )
     annotation_rows = (
-        ('bits', 'Bits', (0, 3, 4)),
-        ('data', 'Data', (9,)),
-        ('name', 'Logical', (1, 2, 5, 6, 7, 8)),
-        ('errors', 'Errors', (10,)),
+        ('dmx_fields', 'Fields', (Ann.BREAK, Ann.MAB,
+            Ann.STARTCODE, Ann.INTERFRAME,
+            Ann.DATABYTE, Ann.INTERPACKET)),
+        ('chans_data', 'Channels data', (Ann.CHANNEL_DATA,)),
+        ('slots_data', 'Slots data', (Ann.SLOT_DATA,)),
+        ('resets', 'Reset sequences', (Ann.RESET,)),
+        ('warnings', 'Warnings', (Ann.WARN,)),
+        ('errors', 'Errors', (Ann.ERROR,)),
     )
 
     def __init__(self):
@@ -57,114 +123,239 @@ class Decoder(srd.Decoder):
 
     def reset(self):
         self.samplerate = None
-        self.sample_usec = None
-        self.run_start = -1
-        self.run_bit = 0
-        self.state = 'FIND BREAK'
+        self.samples_per_usec = None
+        self.last_reset = None
+        self.last_break = None
+        self.packet = None
+        self.last_es = None
+        self.last_frame = None
+        self.start_code = None
 
     def start(self):
         self.out_ann = self.register(srd.OUTPUT_ANN)
+        self.out_python = self.register(srd.OUTPUT_PYTHON)
 
     def metadata(self, key, value):
         if key == srd.SRD_CONF_SAMPLERATE:
             self.samplerate = value
-            self.sample_usec = 1 / value * 1000000
-            self.skip_per_bit = int(4 / self.sample_usec)
-
-    def putr(self, data):
-        self.put(self.run_start, self.samplenum, self.out_ann, data)
-
-    def decode(self):
-        if not self.samplerate:
-            raise SamplerateError('Cannot decode without samplerate.')
-        while True:
-            # Seek for an interval with no state change with a length between
-            # 88 and 1000000 us (BREAK).
-            if self.state == 'FIND BREAK':
-                (dmx,) = self.wait({0: 'h' if self.run_bit == 0 else 'l'})
-                runlen = (self.samplenum - self.run_start) * self.sample_usec
-                if runlen > 88 and runlen < 1000000:
-                    self.putr([1, ['Break']])
-                    self.bit_break = self.run_bit
-                    self.state = 'MARK MAB'
-                    self.channel = 0
-                elif runlen >= 1000000:
-                    # Error condition.
-                    self.putr([10, ['Invalid break length']])
-                self.run_bit = dmx
-                self.run_start = self.samplenum
-            # Directly following the BREAK is the MARK AFTER BREAK.
-            elif self.state == 'MARK MAB':
-                (dmx,) = self.wait({0: 'h' if self.run_bit == 0 else 'l'})
-                self.putr([2, ['MAB']])
-                self.state = 'READ BYTE'
-                self.channel = 0
-                self.bit = 0
-                self.aggreg = dmx
-                self.run_start = self.samplenum
-            # Mark and read a single transmitted byte
-            # (start bit, 8 data bits, 2 stop bits).
-            elif self.state == 'READ BYTE':
-                (dmx,) = self.wait()
-                self.next_sample = self.run_start + (self.bit + 1) * self.skip_per_bit
-                self.aggreg += dmx
-                if self.samplenum != self.next_sample:
-                    continue
-                bit_value = 0 if round(self.aggreg/self.skip_per_bit) == self.bit_break else 1
-
-                if self.bit == 0:
-                    self.byte = 0
-                    self.putr([3, ['Start bit']])
-                    if bit_value != 0:
-                        # (Possibly) invalid start bit, mark but don't fail.
-                        self.put(self.samplenum, self.samplenum,
-                                 self.out_ann, [10, ['Invalid start bit']])
-                elif self.bit >= 9:
-                    self.put(self.samplenum - self.skip_per_bit,
-                        self.samplenum, self.out_ann, [4, ['Stop bit']])
-                    if bit_value != 1:
-                        # Invalid stop bit, mark.
-                        self.put(self.samplenum, self.samplenum,
-                            self.out_ann, [10, ['Invalid stop bit']])
-                        if self.bit == 10:
-                            # On invalid 2nd stop bit, search for new break.
-                            self.run_bit = dmx
-                            self.state = 'FIND BREAK'
-                else:
-                    # Label and process one bit.
-                    self.put(self.samplenum - self.skip_per_bit,
-                        self.samplenum, self.out_ann, [0, [str(bit_value)]])
-                    self.byte |= bit_value << (self.bit - 1)
-
-                # Label a complete byte.
-                if self.bit == 10:
-                    if self.channel == 0:
-                        d = [5, ['Start code']]
-                    else:
-                        d = [6, ['Channel ' + str(self.channel)]]
-                    self.put(self.run_start, self.next_sample, self.out_ann, d)
-                    self.put(self.run_start + self.skip_per_bit,
-                        self.next_sample - 2 * self.skip_per_bit,
-                        self.out_ann, [9, [str(self.byte) + ' / ' + \
-                        str(hex(self.byte))]])
-                    # Continue by scanning the IFT.
-                    self.channel += 1
-                    self.run_start = self.samplenum
-                    self.run_bit = dmx
-                    self.state = 'MARK IFT'
-
-                self.aggreg = dmx
-                self.bit += 1
-            # Mark the INTERFRAME-TIME between bytes / INTERPACKET-TIME between packets.
-            elif self.state == 'MARK IFT':
-                (dmx,) = self.wait({0: 'h' if self.run_bit == 0 else 'l'})
-                if self.channel > 512:
-                    self.putr([8, ['Interpacket']])
-                    self.state = 'FIND BREAK'
-                    self.run_bit = dmx
-                    self.run_start = self.samplenum
-                else:
-                    self.putr([7, ['Interframe']])
-                    self.state = 'READ BYTE'
-                    self.bit = 0
-                    self.run_start = self.samplenum
+            self.samples_per_usec = value / 1000000
+
+    def have_samplerate(self):
+        return bool(self.samplerate)
+
+    def samples_to_usecs(self, count):
+        return count / self.samples_per_usec
+
+    def putg(self, ss, es, data):
+        self.put(ss, es, self.out_ann, data)
+
+    def putpy(self, ss, es, data):
+        self.put(ss, es, self.out_python, data)
+
+    def format_value(self, v):
+        fmt = self.options['format']
+        if fmt == 'dec':
+            return '{:d}'.format(v)
+        if fmt == 'hex':
+            return '{:02X}'.format(v)
+        if fmt == 'bin':
+            return '{:08b}'.format(v)
+        return '{}'.format(v)
+
+    def flush_packet(self):
+        if self.packet:
+            ss, es = self.packet[0][0], self.packet[-1][1]
+            self.putpy(ss, es, ['PACKET', self.packet])
+        self.packet = None
+
+    def flush_reset(self, ss, es):
+        if ss is not None and es is not None:
+            self.putg(ss, es, [Ann.RESET, ['RESET SEQUENCE', 'RESET', 'R']])
+            if self.last_reset and self.have_samplerate():
+                duration = self.samples_to_usecs(es - self.last_reset)
+                if duration > self.options['max_reset_reset']:
+                    txts = ['Excessive RESET to RESET interval', 'RESET to RESET', 'RESET']
+                    self.putg(self.last_reset, es, [Ann.WARN, txts])
+        self.last_reset = es
+
+    def flush_break(self, ss, es):
+        self.putg(ss, es, [Ann.BREAK, ['BREAK', 'B']])
+        if self.have_samplerate():
+            duration = self.samples_to_usecs(es - ss)
+            if duration < self.options['min_break']:
+                txts = ['Short BREAK period', 'Short BREAK', 'BREAK']
+                self.putg(ss, es, [Ann.WARN, txts])
+            if self.last_break:
+                duration = self.samples_to_usecs(ss - self.last_break)
+                if duration < self.options['min_break_break']:
+                    txts = ['Short BREAK to BREAK interval', 'Short BREAK to BREAK', 'BREAK']
+                    self.putg(ss, es, [Ann.WARN, txts])
+        self.last_break = ss
+        self.last_es = es
+
+    def flush_mark(self, ss, es, is_mab = False, is_if = False, is_ip = False):
+        '''Handle several kinds of MARK conditions.'''
+
+        if ss is None or es is None or ss >= es:
+            return
+
+        if is_mab:
+            ann = Ann.MAB
+            txts = ['MARK AFTER BREAK', 'MAB']
+        elif is_if:
+            ann = Ann.INTERFRAME
+            txts = ['INTER FRAME', 'IF']
+        elif is_ip:
+            ann = Ann.INTERPACKET
+            txts = ['INTER PACKET', 'IP']
+        else:
+            return
+        self.putg(ss, es, [ann, txts])
+
+        if self.have_samplerate():
+            duration = self.samples_to_usecs(es - ss)
+            if duration > self.options['max_mark']:
+                txts = ['Excessive MARK length', 'MARK length', 'MARK']
+                self.putg(ss, es, [Ann.ERROR, txts])
+
+    def flush_frame(self, ss, es, value, valid):
+        '''Handle UART frame content. Accumulate DMX packet.'''
+
+        if not valid:
+            txts = ['Invalid frame', 'Frame']
+            self.putg(ss, es, [Ann.ERROR, txts])
+
+        self.last_es = es
+
+        # Cease packet inspection before first BREAK.
+        if not self.last_break:
+            return
+
+        # Accumulate the sequence of bytes for the current DMX frame.
+        # Emit the annotation at the "DMX fields" level.
+        is_start = self.packet is None
+        if is_start:
+            self.packet = []
+        slot_nr = len(self.packet)
+        item = (ss, es, value, valid)
+        self.packet.append(item)
+        if is_start:
+            # Slot 0, the start code. Determines the DMX frame type.
+            self.start_code = value
+            ann = Ann.STARTCODE
+            val_text = self.format_value(value)
+            txts = [
+                'STARTCODE {}'.format(val_text),
+                'START {}'.format(val_text),
+                '{}'.format(val_text),
+            ]
+        else:
+            # Slot 1+, the payload bytes.
+            ann = Ann.DATABYTE
+            val_text = self.format_value(value)
+            txts = [
+                'DATABYTE {:d}: {}'.format(slot_nr, val_text),
+                'DATA {:d}: {}'.format(slot_nr, val_text),
+                'DATA {}'.format(val_text),
+                '{}'.format(val_text),
+            ]
+        self.putg(ss, es, [ann, txts])
+
+        # Tell channel data for peripherals from arbitrary slot values.
+        # Can get extended for other start code types in case protocol
+        # extensions are handled here and not in stacked decoders.
+        if is_start:
+            ann = None
+        elif self.start_code == 0:
+            # Start code was 0. Slots carry values for channels.
+            # Optionally suppress zero-values to make used channels
+            # stand out, to help users focus their attention.
+            ann = Ann.CHANNEL_DATA
+            if value == 0 and self.options['show_zero'] == 'no':
+                ann = None
+            else:
+                val_text = self.format_value(value)
+                txts = [
+                    'CHANNEL {:d}: {}'.format(slot_nr, val_text),
+                    'CH {:d}: {}'.format(slot_nr, val_text),
+                    'CH {}'.format(val_text),
+                    '{}'.format(val_text),
+                ]
+        else:
+            # Unhandled start code. Provide "anonymous" values.
+            ann = Ann.SLOT_DATA
+            val_text = self.format_value(value)
+            txts = [
+                'SLOT {:d}: {}'.format(slot_nr, val_text),
+                'SL {:d}: {}'.format(slot_nr, val_text),
+                'SL {}'.format(val_text),
+                '{}'.format(val_text),
+            ]
+        if ann is not None:
+            self.putg(ss, es, [ann, txts])
+
+        if is_start and value == 0:
+            self.flush_reset(self.last_break, es)
+
+    def handle_break(self, ss, es):
+        '''Handle UART BREAK conditions.'''
+
+        # Check the last frame before BREAK if one was queued. It could
+        # have been "invalid" since the STOP bit check failed. If there
+        # is an invalid frame which happens to start at the start of the
+        # BREAK condition, then discard it. Otherwise flush its output.
+        last_frame = self.last_frame
+        self.last_frame = None
+        frame_invalid = last_frame and not last_frame[3]
+        frame_zero_data = last_frame and last_frame[2] == 0
+        frame_is_break = last_frame and last_frame[0] == ss
+        if frame_invalid and frame_zero_data and frame_is_break:
+            last_frame = None
+        if last_frame is not None:
+            self.flush_frame(*last_frame)
+
+        # Handle inter-packet MARK (works for zero length, too).
+        self.flush_mark(self.last_es, ss, is_ip = True)
+
+        # Handle accumulated packets.
+        self.flush_packet()
+        self.packet = None
+
+        # Annotate the BREAK condition. Start accumulation of a packet.
+        self.flush_break(ss, es)
+
+    def handle_frame(self, ss, es, value, valid):
+        '''Handle UART data frames.'''
+
+        # Flush previously deferred frame (if available). Can't have been
+        # BREAK if another data frame follows.
+        last_frame = self.last_frame
+        self.last_frame = None
+        if last_frame:
+            self.flush_frame(*last_frame)
+
+        # Handle inter-frame MARK (works for zero length, too).
+        is_mab = self.last_break and self.packet is None
+        is_if = self.packet
+        self.flush_mark(self.last_es, ss, is_mab = is_mab, is_if = is_if)
+
+        # Defer handling of invalid frames, because they may start a new
+        # BREAK which we will only learn about much later. Immediately
+        # annotate valid frames.
+        if valid:
+            self.flush_frame(ss, es, value, valid)
+        else:
+            self.last_frame = (ss, es, value, valid)
+
+    def decode(self, ss, es, data):
+        # Lack of a sample rate in the input capture only disables the
+        # optional warnings about exceeded timespans here at the DMX512
+        # decoder level. That the lower layer UART decoder depends on a
+        # sample rate is handled there, and is not relevant here.
+
+        ptype, rxtx, pdata = data
+        if ptype == 'BREAK':
+            self.handle_break(ss, es)
+        elif ptype == 'FRAME':
+            value, valid = pdata
+            self.handle_frame(ss, es, value, valid)