bin/dmx.py
changeset 74 9031dafa39d6
parent 73 3c25f32c92fa
equal deleted inserted replaced
73:3c25f32c92fa 74:9031dafa39d6
     1 #!/usr/bin/env python
     1 #!/usr/bin/env python
     2 
     2 
     3 import collections
     3 import qmsk.dmx
     4 import serial
       
     5 import time
       
     6 
     4 
     7 import logging; log = logging.getLogger('dmx')
     5 import logging; log = logging.getLogger('dmx')
     8 
       
     9 class DMXError (Exception) :
       
    10     def __init__ (self, **kwargs) :
       
    11         self.kwargs = kwargs
       
    12 
       
    13     def __str__ (self) :
       
    14         return self.__doc__.strip().format(**self.kwargs)
       
    15 
       
    16 class DMXCommandError (DMXError) :
       
    17     """
       
    18         Command {cmd!r} failed: {out!r}
       
    19     """
       
    20 
       
    21 class DMXUnknownCommandError (DMXError) :
       
    22     """
       
    23         Unknown command: {cmd!r}
       
    24     """
       
    25 
       
    26 class DMX (object) :
       
    27     """
       
    28         Arudino-based DMX controller using src/hello-dmx.c over the serial port.
       
    29     """
       
    30 
       
    31     SERIAL = '/dev/arduino'
       
    32     SERIAL_BAUD = 9600
       
    33     SERIAL_TIMEOUT = 1.0
       
    34 
       
    35     @classmethod
       
    36     def open (cls, path, baud=SERIAL_BAUD, timeout=SERIAL_TIMEOUT) :
       
    37         return cls(serial.Serial(path, baud, timeout=timeout))
       
    38 
       
    39     def __init__ (self, io) :
       
    40         self.io = io
       
    41 
       
    42         # XXX: bug
       
    43         self.io.write('\r')
       
    44         self.io.flush()
       
    45         self.io.read(1)
       
    46 
       
    47     def _arg (self, arg) :
       
    48         if isinstance(arg, str) :
       
    49             value, = arg
       
    50             value = ord(value)
       
    51         elif isinstance(arg, int) :
       
    52             value = arg
       
    53         else :
       
    54             raise ValueError(arg)
       
    55 
       
    56         if 0 <= value <= 255 :
       
    57             return str(value)
       
    58         else :
       
    59             raise ValueError(value)
       
    60 
       
    61     def __call__ (self, cmd, *args) :
       
    62         out = cmd + ' ' + ' '.join(self._arg(arg) for arg in args) + '\r'
       
    63         
       
    64         log.info("%s", out)
       
    65         
       
    66         self.io.write(out)
       
    67         self.io.flush()
       
    68 
       
    69         ret = self.io.read(len(out))
       
    70 
       
    71         if '!' in ret :
       
    72             raise DMXCommandError(cmd=out, out=ret)
       
    73 
       
    74         elif '?' in ret :
       
    75             raise DMXUnknownCommandError(cmd=cmd)
       
    76 
       
    77     def clear (self) :
       
    78         """
       
    79             Set dmx = [ ]
       
    80 
       
    81             i.e. start transmitting zero-length DMX packets.
       
    82             For most lights, this seems to be equivalent to losing the DMX signal, and they retain their old state.
       
    83         """
       
    84 
       
    85         self('c')
       
    86 
       
    87     def zero (self) :
       
    88         """
       
    89             Set dmx = [0, ...]
       
    90 
       
    91             Uses the maximum DMX packet length available.
       
    92         """
       
    93 
       
    94         self('z')
       
    95 
       
    96     def out (self, *values) :
       
    97         """
       
    98             Set dmx = (value, ...)
       
    99         """
       
   100 
       
   101         self('o', *values)
       
   102 
       
   103     def set (self, start, *values) :
       
   104         """
       
   105             Set dmx[start:] = value
       
   106         """
       
   107 
       
   108         self('s', start, *values)
       
   109 
       
   110     def fill (self, start, end, *values) :
       
   111         """
       
   112             Set dmx[start:end] to repetitions of (value, ...)
       
   113         """
       
   114 
       
   115         self('f', start, end, *values)
       
   116 
       
   117     def range (self, start, stop, step, value) :
       
   118         """
       
   119             Set dmx[start:end:step] = value
       
   120         """
       
   121 
       
   122         self('r', start, stop, step, value)
       
   123 
       
   124     def __setitem__ (self, index, value) :
       
   125         if isinstance(value, collections.Sequence) :
       
   126             values = tuple(value)
       
   127         else :
       
   128             values = (value, )
       
   129 
       
   130         if isinstance(index, slice) :
       
   131             if index.start and index.stop and index.step :
       
   132                 # XXX: single
       
   133                 self.range(index.start, index.stop, index.step, value)
       
   134 
       
   135             elif index.start and index.stop :
       
   136                 self.fill(index.start, index.stop, *values)
       
   137 
       
   138             elif index.start :
       
   139                 self.set(index.start, *values)
       
   140 
       
   141             else :
       
   142                 raise IndexError("invalid slice: %s" % (index, ))
       
   143         
       
   144         else :
       
   145             # simple set
       
   146             self.set(index, *values)
       
   147 
     6 
   148 import argparse
     7 import argparse
   149 
     8 
   150 def main (argv) :
     9 def main (argv) :
   151     parser = argparse.ArgumentParser()
    10     parser = argparse.ArgumentParser()
   152     parser.add_argument('--serial', default=DMX.SERIAL,
    11     parser.add_argument('--serial', default=qmsk.dmx.DMX.SERIAL,
   153             help="Path to /dev/tty*")
    12             help="Path to /dev/tty*")
   154     parser.add_argument('--zero', action='store_true',
    13     parser.add_argument('--zero', action='store_true',
   155             help="Zero output before setting")
    14             help="Zero output before setting")
   156     parser.add_argument('--start', type=int,
    15     parser.add_argument('--start', type=int,
   157             help="Set from start offset")
    16             help="Set from start offset")
   165 
    24 
   166     options = parser.parse_args(argv[1:])
    25     options = parser.parse_args(argv[1:])
   167 
    26 
   168     logging.basicConfig(level=logging.DEBUG)
    27     logging.basicConfig(level=logging.DEBUG)
   169 
    28 
   170     dmx = DMX.open(options.serial)
    29     dmx = qmsk.dmx.DMX.open(options.serial)
   171 
    30 
   172     if options.zero :
    31     if options.zero :
   173         dmx.zero()
    32         dmx.zero()
   174 
    33 
   175 
    34