## 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
## 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, write to the Free Software
-## Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+## 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 = 2
+ api_version = 3
id = 'dmx512'
name = 'DMX512'
longname = 'Digital MultipleX 512'
- desc = 'Professional lighting control protocol.'
+ desc = 'Digital MultipleX 512 (DMX512) lighting protocol.'
license = 'gplv2+'
- inputs = ['logic']
+ inputs = ['uart']
outputs = ['dmx512']
- channels = (
- {'id': 'dmx', 'name': 'DMX data', 'desc': 'Any DMX data line'},
+ tags = ['Embedded/industrial', 'Lighting']
+ 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):
+ self.reset()
+
+ def reset(self):
self.samplerate = None
- self.sample_usec = None
- self.samplenum = -1
- 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)
+ 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.'''
- def putr(self, data):
- self.put(self.run_start, self.samplenum, self.out_ann, data)
+ 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):
- if not self.samplerate:
- raise SamplerateError('Cannot decode without samplerate.')
- for (self.samplenum, pins) in data:
- # Seek for an interval with no state change with a length between
- # 88 and 1000000 us (BREAK).
- if self.state == 'FIND BREAK':
- if self.run_bit == pins[0]:
- continue
- 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 = pins[0]
- self.run_start = self.samplenum
- # Directly following the BREAK is the MARK AFTER BREAK.
- elif self.state == 'MARK MAB':
- if self.run_bit == pins[0]:
- continue
- self.putr([2, ['MAB']])
- self.state = 'READ BYTE'
- self.channel = 0
- self.bit = 0
- self.aggreg = pins[0]
- 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':
- self.next_sample = self.run_start + (self.bit + 1) * self.skip_per_bit
- self.aggreg += pins[0]
- 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 = pins[0]
- 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 = pins[0]
- self.state = 'MARK IFT'
-
- self.aggreg = pins[0]
- self.bit += 1
- # Mark the INTERFRAME-TIME between bytes / INTERPACKET-TIME between packets.
- elif self.state == 'MARK IFT':
- if self.run_bit == pins[0]:
- continue
- if self.channel > 512:
- self.putr([8, ['Interpacket']])
- self.state = 'FIND BREAK'
- self.run_bit = pins[0]
- self.run_start = self.samplenum
- else:
- self.putr([7, ['Interframe']])
- self.state = 'READ BYTE'
- self.bit = 0
- self.run_start = self.samplenum
+ # 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)