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