From: Jeremy Swanson Date: Thu, 16 Feb 2017 16:29:47 +0000 (+0000) Subject: Add a DALI protocol decoder. X-Git-Tag: libsigrokdecode-0.5.0~54 X-Git-Url: https://sigrok.org/gitaction?a=commitdiff_plain;h=b26c713b6a1650eba0e7808672f290813c893881;p=libsigrokdecode.git Add a DALI protocol decoder. --- diff --git a/decoders/dali/__init__.py b/decoders/dali/__init__.py new file mode 100644 index 0000000..564dbee --- /dev/null +++ b/decoders/dali/__init__.py @@ -0,0 +1,24 @@ +## +## This file is part of the libsigrokdecode project. +## +## Copyright (C) 2015 Jeremy Swanson +## +## 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 . +## + +''' +DALI is a biphase/manchester based lighting control protocol. +''' + +from .pd import Decoder diff --git a/decoders/dali/lists.py b/decoders/dali/lists.py new file mode 100644 index 0000000..24e6bc4 --- /dev/null +++ b/decoders/dali/lists.py @@ -0,0 +1,98 @@ +## +## This file is part of the libsigrokdecode project. +## +## Copyright (C) 2015 Jeremy Swanson +## +## 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 . +## + +# DALI Extended commands +extendedCommands = { + 0xA1: ['Terminate special processes', 'Terminate'], + 0xA3: ['DTR = DATA', 'DTR'], + 0xA5: ['INITIALISE', 'INIT'], + 0xA7: ['RANDOMISE', 'RAND'], + 0xA9: ['COMPARE', 'COMP'], + 0xAB: ['WITHDRAW', 'WDRAW'], + 0xB1: ['SET SEARCH H', 'SAH'], + 0xB3: ['SET SEARCH M', 'SAM'], + 0xB5: ['SET SEARCH L', 'SAL'], + 0xB7: ['Program Short Address', 'ProgSA'], + 0xB9: ['Verify Short Address', 'VfySA'], + 0xBB: ['Query Short Address', 'QryShort'], + 0xBD: ['Physical Selection', 'PysSel'], + 0xC1: ['Enable Device Type X', 'EnTyp'], + 0xC3: ['DTR1 = DATA', 'DTR1'], + 0xC5: ['DTR2 = DATA', 'DTR2'], + 0xC7: ['Write Memory Location', 'WRI'], +} + +# List of commands +DALICommands = { + 0x00: ['Immediate Off', 'IOFF'], + 0x01: ['Up 200ms', 'Up'], + 0x02: ['Down 200ms', 'Down'], + 0x03: ['Step Up', 'Step+'], + 0x04: ['Step Down', 'Step-'], + 0x05: ['Recall Maximum Level', 'Recall Max'], + 0x06: ['Recall Minimum Level', 'Recall Min'], + 0x07: ['Step down and off', 'Down Off'], + 0x08: ['Step ON and UP', 'On Up'], + 0x20: ['Reset', 'Rst'], + 0x21: ['Store Dim Level in DTR', 'Level -> DTR'], + 0x2A: ['Store DTR as Max Level', 'DTR->Max'], + 0x2B: ['Store DTR as Min Level', 'DTR->Min'], + 0x2C: ['Store DTR as Fail Level', 'DTR->Fail'], + 0x2D: ['Store DTR as Power On Level', 'DTR->Poweron'], + 0x2E: ['Store DTR as Fade Time', 'DTR->Fade'], + 0x2F: ['Store DTR as Fade Rate', 'DTR->Rate'], + 0x80: ['Store DTR as Short Address', 'DTR->Add'], + 0x81: ['Enable Memory Write', 'WEn'], + 0x90: ['Query Status', 'Status'], + 0x91: ['Query Ballast', 'Ballast'], + 0x92: ['Query Lamp Failure', 'LmpFail'], + 0x93: ['Query Power On', 'Power On'], + 0x94: ['Query Limit Error', 'Limit Err'], + 0x95: ['Query Reset', 'Reset State'], + 0x96: ['Query Missing Short Address', 'NoSrt'], + 0x97: ['Query Version', 'Ver'], + 0x98: ['Query DTR', 'GetDTR'], + 0x99: ['Query Device Type', 'Type'], + 0x9A: ['Query Physical Minimum', 'PhysMin'], + 0x9B: ['Query Power Fail', 'PowerFailed'], + 0x9C: ['Query DTR1', 'GetDTR1'], + 0x9D: ['Query DTR2', 'GetDTR2'], + 0xA0: ['Query Level', 'GetLevel'], + 0xA1: ['Query Max Level', 'GetMax'], + 0xA2: ['Query Min Level', 'GetMin'], + 0xA3: ['Query Power On', 'GetPwrOn'], + 0xA4: ['Query Fail Level', 'GetFail'], + 0xA5: ['Query Fade Rate', 'GetRate'], + 0xA6: ['Query Power Fail', 'PwrFail'], + 0xC0: ['Query Groups 0-7', 'GetGrpsL'], + 0xC1: ['Query Groups 7-15', 'GetGrpsH'], + 0xC2: ['Query BRNH', 'BRNH'], + 0xC3: ['Query BRNM', 'BRNM'], + 0xC4: ['Query BRNL', 'BRNL'], + 0xC5: ['Query Memory', 'GetMem'], +} + +# DALI device type 8 +DALIDeviceType8 = { + 0xE0: ['Set Temp X-Y Coordinate', 'Set X-Y'], + 0xE2: ['Activate Colour Set point', 'Activate SetPoint'], + 0xE7: ['Set Colour Temperature Tc', 'DTRs->ColTemp'], + 0xF9: ['Query Features', 'QryFeats'], + 0xFA: ['Query Current Setpoint Colour', 'GetSetPoint'], +} diff --git a/decoders/dali/pd.py b/decoders/dali/pd.py new file mode 100644 index 0000000..eb76f73 --- /dev/null +++ b/decoders/dali/pd.py @@ -0,0 +1,262 @@ +## +## This file is part of the libsigrokdecode project. +## +## Copyright (C) 2015 Jeremy Swanson +## +## 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 . +## + +import sigrokdecode as srd +from .lists import * + +class SamplerateError(Exception): + pass + +class Decoder(srd.Decoder): + api_version = 2 + id = 'dali' + name = 'DALI' + longname = 'Digital Addressable Lighting Interface' + desc = 'DALI lighting control protocol.' + license = 'gplv2+' + inputs = ['logic'] + outputs = ['dali'] + channels = ( + {'id': 'dali', 'name': 'DALI', 'desc': 'DALI data line'}, + ) + options = ( + {'id': 'polarity', 'desc': 'Polarity', 'default': 'active-low', + 'values': ('active-low', 'active-high')}, + ) + annotations = ( + ('bit', 'Bit'), + ('startbit', 'Startbit'), + ('sbit', 'Select bit'), + ('ybit', 'Individual or group'), + ('address', 'Address'), + ('command', 'Command'), + ('reply', 'Reply data'), + ('raw', 'Raw data'), + ) + annotation_rows = ( + ('bits', 'Bits', (0,)), + ('raw', 'Raw data', (7,)), + ('fields', 'Fields', (1, 2, 3, 4, 5, 6,)), + ) + + def __init__(self): + self.samplerate = None + self.samplenum = None + self.edges, self.bits, self.ss_es_bits = [], [], [] + self.state = 'IDLE' + self.nextSamplePoint = None + self.nextSample = None + self.devType = None + + def start(self): + self.out_ann = self.register(srd.OUTPUT_ANN) + self.old_ir = 1 if self.options['polarity'] == 'active-low' else 0 + + def metadata(self, key, value): + if key == srd.SRD_CONF_SAMPLERATE: + self.samplerate = value + # One bit: 833.33us (one half low, one half high). + # This is how may samples are in 1TE. + self.halfbit = int((self.samplerate * 0.0008333) / 2.0) + + def putb(self, bit1, bit2, data): + ss, es = self.ss_es_bits[bit1][0], self.ss_es_bits[bit2][1] + self.put(ss, es, self.out_ann, data) + + def handle_bits(self, length): + a, c, f, g, b = 0, 0, 0, 0, self.bits + # Individual raw bits. + for i in range(length): + if i == 0: + ss = max(0, self.bits[0][0]) + else: + ss = self.ss_es_bits[i - 1][1] + es = self.bits[i][0] + (self.halfbit * 2) + self.ss_es_bits.append([ss, es]) + self.putb(i, i, [0, ['%d' % self.bits[i][1]]]) + # Bits[0:0]: Startbit + s = ['Startbit: %d' % b[0][1], 'ST: %d' % b[0][1], 'ST', 'S', 'S'] + self.putb(0, 0, [1, s]) + self.putb(0, 0, [7, s]) + # Bits[1:8] + for i in range(8): + f |= (b[1 + i][1] << (7 - i)) + if length == 9: # BACKWARD Frame + s = ['Reply: %02X' % f, 'Rply: %02X' % f, + 'Rep: %02X' % f, 'R: %02X' % f, 'R'] + self.putb(1, 8, [7, s]) + s = ['Reply: %d' % f, 'Rply: %d' % f, + 'Rep: %d' % f, 'R: %d' % f, 'R'] + self.putb(1, 8, [6, s]) + return + + # FORWARD FRAME + # Bits[9:16]: Command/data (MSB-first) + for i in range(8): + c |= (b[9 + i][1] << (7 - i)) + # Raw output + s = ['Raw data: %02X' % f, 'Raw: %02X' % f, + 'Raw: %02X' % f, 'R: %02X' % f, 'R'] + self.putb(1, 8, [7, s]) + s = ['Raw data: %02X' % c, 'Raw: %02X' % c, + 'Raw: %02X' % c, 'R: %02X' % c, 'R'] + self.putb(9, 16, [7, s]) + + # Bits[8:8]: Select bit + # s = ['Selectbit: %d' % b[8][1], 'SEL: %d' % b[8][1], 'SEL', 'SE', 'S'] + if b[8][1] == 1: + s = ['Command', 'Comd', 'COM', 'CO', 'C'] + else: + s = ['Arc Power Level', 'Arc Pwr', 'ARC', 'AC', 'A'] + self.putb(8, 8, [1, s]) + + # f &= 254 # Clear the select bit. + if f >= 254: # BROADCAST + s = ['BROADCAST', 'Brdcast', 'BC', 'B', 'B'] + self.putb(1, 7, [5, s]) + elif f >= 160: # Extended command 0b10100000 + if f == 0xC1: # DALI_ENABLE_DEVICE_TYPE_X + self.devType = -1 + x = extendedCommands.get(f, ['Unknown', 'Unk']) + s = ['Extended Command: %02X (%s)' % (f, x[0]), + 'XC: %02X (%s)' % (f, x[1]), + 'XC: %02X' % f, 'X: %02X' % f, 'X'] + self.putb(1, 8, [5, s]) + elif f >= 128: # Group + # Bits[1:1]: Ybit + s = ['YBit: %d' % b[1][1], 'YB: %d' % b[1][1], 'YB', 'Y', 'Y'] + self.putb(1, 1, [3, s]) + g = (f & 127) >> 1 + s = ['Group address: %d' % g, 'Group: %d' % g, + 'GP: %d' % g, 'G: %d' % g, 'G'] + self.putb(2,7, [4, s]) + else: # Short address + # Bits[1:1]: Ybit + s = ['YBit: %d' % b[1][1], 'YB: %d' % b[1][1], 'YB', 'Y', 'Y'] + self.putb(1, 1, [3, s]) + a = f >> 1 + # x = system.get(a, ['Unknown', 'Unk']) + s = ['Short address: %d' % a, 'Addr: %d' % a, + 'Addr: %d' % a, 'A: %d' % a, 'A'] + self.putb(2, 7, [4, s]) + + # Bits[9:16]: Command/data (MSB-first) + if f >= 160 and f < 254: + if self.devType == -1: + self.devType = c + s = ['Type: %d' % c, 'Typ: %d' % c, + 'Typ: %d' % c, 'T: %d' % c, 'D'] + else: + self.devType = None + s = ['Data: %d' % c, 'Dat: %d' % c, + 'Dat: %d' % c, 'D: %d' % c, 'D'] + elif b[8][1] == 1: + un = c & 0xF0 + ln = c & 0x0F + if un == 0x10: # Set scene command + x = ['Recall Scene %d' % ln, 'SC %d' % ln] + elif un == 0x40: + x = ['Store DTR as Scene %d' % ln, 'SC %d = DTR' % ln] + elif un == 0x50: + x = ['Delete Scene %d' % ln, 'DEL SC %d' % ln] + elif un == 0x60: + x = ['Add to Group %d' % ln, 'Grp %d Add' % ln] + elif un == 0x70: + x = ['Remove from Group %d' % ln, 'Grp %d Del' % ln] + elif un == 0xB0: + x = ['Query Scene %d Level' % ln, 'Sc %d Level' % ln] + elif c >= 224: # Application specific commands + if self.devType == 8: + x = DALIDeviceType8.get(c, ['Unknown App', 'Unk']) + else: + x = ['Application Specific Command %d' % c, 'App Cmd %d' % c] + else: + x = DALICommands.get(c, ['Unknown', 'Unk']) + s = ['Command: %d (%s)' % (c, x[0]), 'Com: %d (%s)' % (c, x[1]), + 'Com: %d' % c, 'C: %d' % c, 'C'] + else: + s = ['Arc Power Level: %d' % c, 'Level: %d' % c, + 'Lev: %d' % c, 'L: %d' % c, 'L'] + self.putb(9, 16, [5, s]) + + def reset_decoder_state(self): + self.edges, self.bits, self.ss_es_bits = [], [], [] + self.state = 'IDLE' + # self.devType = None + + def decode(self, ss, es, data): + if not self.samplerate: + raise SamplerateError('Cannot decode without samplerate.') + bit = 0; + for (self.samplenum, pins) in data: + self.ir = pins[0] + # data.itercnt += 1 + # data.logic_mask = 1 + # data.cur_pos = self.samplenum + # data.edge_index = -1 + if self.options['polarity'] == 'active-high': + self.ir ^= 1 # Invert. + + # State machine. + if self.state == 'IDLE': + # Wait for any edge (rising or falling). + if self.old_ir == self.ir: + # data.exp_logic = self.exp_logic + # data.logic_mask = 1 + # logic.cur_pos = self.samplenum + continue + self.edges.append(self.samplenum) + self.state = 'PHASE0' + self.old_ir = self.ir + # Get the next sample point. + # self.nextSamplePoint = self.samplenum + int(self.halfbit / 2) + self.old_ir = self.ir + # bit = self.ir + # data.itercnt += int((self.halfbit - 1) * 0.5) + continue + + # if(self.samplenum == self.nextSamplePoint): + # bit = self.ir + # continue + + if (self.old_ir != self.ir): + self.edges.append(self.samplenum) + elif (self.samplenum == (self.edges[-1] + int(self.halfbit * 1.5))): + self.edges.append(self.samplenum - int(self.halfbit * 0.5)) + else: + continue + + bit = self.old_ir + if self.state == 'PHASE0': + self.phase0 = bit + self.state = 'PHASE1' + elif self.state == 'PHASE1': + if (bit == 1) and (self.phase0 == 1): # Stop bit + if len(self.bits) == 17 or len(self.bits) == 9: + # Forward or Backward + self.handle_bits(len(self.bits)) + self.reset_decoder_state() # Reset upon errors. + continue + else: + self.bits.append([self.edges[-3], bit]) + self.state = 'PHASE0' + + # self.nextSamplePoint = self.edges[-1] + int(self.halfbit / 2) + + self.old_ir = self.ir