X-Git-Url: https://sigrok.org/gitweb/?a=blobdiff_plain;f=decoders%2Fdmx512%2Fpd.py;h=1bcca20ff67cd108882fbf528b3fa4628655ad02;hb=HEAD;hp=081758efcc57fee8436fbfc7ae064dadb3e1a77c;hpb=2787cf2abc0187679e87d3735ca3e64c2a1a91c8;p=libsigrokdecode.git diff --git a/decoders/dmx512/pd.py b/decoders/dmx512/pd.py index 081758e..a0cd83f 100644 --- a/decoders/dmx512/pd.py +++ b/decoders/dmx512/pd.py @@ -2,6 +2,7 @@ ## This file is part of the libsigrokdecode project. ## ## Copyright (C) 2016 Fabian J. Stumpf +## Copyright (C) 2019-2020 Gerhard Sittig ## ## 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 @@ -17,8 +18,54 @@ ## along with this program; if not, see . ## +''' +OUTPUT_PYTHON format: + +Packet: +[, ] + +This is the list of codes and their respective 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'] + 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 = ( - ('name', 'Logical', (1, 2, 5, 6, 7, 8)), - ('data', 'Data', (9,)), - ('bits', 'Bits', (0, 3, 4)), - ('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)