qmsk.dmx: new Head-based model/view; output/updates not yet implemented
authorTero Marttila <terom@paivola.fi>
Thu, 01 May 2014 23:34:20 +0300
changeset 83 136e210fce82
parent 82 b5878197d017
child 84 9ebf1a2cee3a
qmsk.dmx: new Head-based model/view; output/updates not yet implemented
bin/dmx-web.py
qmsk/dmx/control.py
qmsk/dmx/heads.py
qmsk/dmx/web.py
static/color-slider.js
static/dmx.css
static/dmx.js
--- a/bin/dmx-web.py	Mon Apr 21 00:20:27 2014 +0300
+++ b/bin/dmx-web.py	Thu May 01 23:34:20 2014 +0300
@@ -9,6 +9,16 @@
 
 import optparse
 
+def dmx_heads (options) :
+    from qmsk.dmx import heads
+    
+    return {
+        'ledpar':   heads.Stairville_LEDPar56(1),
+        'par1':     heads.Dimmer(5),
+        'par2':     heads.Dimmer(6),
+        'ledbar':   heads.AmericanDJ_MegaTri60_Mode2(10),
+    }
+
 def main (argv) :
     """
         DMX web control.
@@ -18,7 +28,7 @@
     parser.add_option_group(pvl.args.parser(parser))
     parser.add_option_group(pvl.web.args.parser(parser))
 
-    parser.add_option('--dmx-serial', default=qmsk.dmx.DMX.SERIAL,
+    parser.add_option('--dmx-serial', default=None,
             help="Path to /dev/tty*")
 
     options, args = parser.parse_args(argv[1:])
@@ -26,11 +36,14 @@
     pvl.args.apply(options)
 
     # dmx
-    dmx = qmsk.dmx.DMX.open(options.dmx_serial)
-    dmx.zero()
+    if options.dmx_serial :
+        dmx = qmsk.dmx.DMX.open(options.dmx_serial)
+        #dmx.zero()
+    else :
+        dmx = None
     
     # app
-    app = qmsk.dmx.web.DMXWebApplication(dmx)
+    app = qmsk.dmx.web.DMXWebApplication(dmx, dmx_heads(options))
 
     pvl.web.args.main(options, app)
 
--- a/qmsk/dmx/control.py	Mon Apr 21 00:20:27 2014 +0300
+++ b/qmsk/dmx/control.py	Thu May 01 23:34:20 2014 +0300
@@ -2,6 +2,10 @@
 import logging; log = logging.getLogger('qmsk.dmx.control')
 import serial
 
+"""
+    Low-level DMX channel output.
+"""
+
 class DMXError (Exception) :
     def __init__ (self, **kwargs) :
         self.kwargs = kwargs
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/qmsk/dmx/heads.py	Thu May 01 23:34:20 2014 +0300
@@ -0,0 +1,82 @@
+"""
+    High-level DMX output.
+"""
+
+class Head (object) :
+    CHANNELS = None
+
+    def __init__ (self, channel) :
+        self.channel = channel
+        self.channels = self.init()
+        self.attrs = { }
+
+    def init (self) :
+        if self.CHANNELS :
+            return self.CHANNELS
+        else :
+            raise NotImplementedError()
+
+    def __setitem__ (self, attr, value) :
+        if attr in self.channels :
+            self.attrs[attr] = value
+        else :
+            raise KeyError(attr)
+
+    def __getitem__ (self, attr) :
+        if attr in self.channels :
+            return self.attrs.get(attr, 0)
+        else :
+            raise KeyError(attr)
+ 
+    def alpha (self, alpha=None) :
+        if alpha is not None :
+            self['alpha'] = alpha
+        
+        if 'alpha' in self.channels :
+            return dict(alpha=self['alpha'])
+        else :
+            return None
+
+    def color (self, red=None, green=None, blue=None) :
+        if red is not None :
+            self['red'] = red
+        if green is not None :
+            self['green'] = green
+        if blue is not None :
+            self['blue'] = blue
+        
+        if 'red' in self.channels and 'green' in self.channels and 'blue' in self.channels :
+            return dict(red=self['red'], green=self['green'], blue=self['blue'])
+        else :
+            return None
+
+    def __iter__ (self) :
+        for attr in self.channels :
+            yield self.attrs.get(attr, 0)
+
+    def __call__ (self, dmx) :
+        dmx[self.channel] = tuple(self)
+
+class Stairville_LEDPar56 (Head) :
+    CHANNELS = [
+        'control1',
+        'red',
+        'green',
+        'blue',
+        'control2',
+    ]
+
+class Dimmer (Head) :
+    CHANNELS = [
+        'alpha',
+    ]
+
+class AmericanDJ_MegaTri60_Mode2 (Head) :
+    CHANNELS = [
+        'red',
+        'green',
+        'blue',
+        'control',
+        'alpha',
+    ]
+
--- a/qmsk/dmx/web.py	Mon Apr 21 00:20:27 2014 +0300
+++ b/qmsk/dmx/web.py	Thu May 01 23:34:20 2014 +0300
@@ -3,6 +3,39 @@
 
 import logging; log = logging.getLogger('qmsk.dmx.web')
 
+def colorize (x, red, green, blue, alpha=1.0) :
+    return x(style='background-color: rgba({red}, {green}, {blue}, {alpha:0.2f})'.format(red=red, green=green, blue=blue, alpha=alpha))
+
+def input (head, name, value) :
+    return html.input(
+            type        = 'text',
+            name        = '-'.join([head, name]),
+
+            id          = '-'.join([head, name]),
+            class_      = 'form-control dmx-input dmx-input-{name}'.format(name=name),
+
+            placeholder = name,
+            value       = '{v:d}'.format(v=value) if value else None,
+    )
+
+def color_input (head, c, value) :
+    color = dict(red=0, green=0, blue=0, alpha=0)
+
+    color[c] = 255
+    if value :
+        color[alpha] = value / 255.0
+
+    return colorize(input(head, c, value), **color)
+
+def slider (head, name) :
+    return html.div(id='-'.join([head, name, 'slider']), class_='dmx-slider dmx-slider-{name}'.format(name=name))
+
+def color_slider (head, c) :
+    return slider(head, c)
+
+def head_color (head, value) :
+    return html.div(class_='dmx-color-background')(colorize(html.div(id='-'.join([head, 'color']), class_='dmx-color')(' '), **value))
+
 class Handler (pvl.web.Handler) :
     # Bootstrap
     DOCTYPE = 'html'
@@ -11,6 +44,8 @@
     CSS = (
             '//code.jquery.com/ui/1.10.4/themes/ui-darkness/jquery-ui.css',
             '//netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css',
+
+            '/static/dmx.css',
     )
     JS = (
             '//code.jquery.com/jquery-2.1.0.js',
@@ -18,125 +53,71 @@
             '//netdna.bootstrapcdn.com/bootstrap/3.0.3/js/bootstrap.min.js',
             
             '/static/color-slider.js',
+            '/static/dmx.js',
     )
 
-    STYLE = """
-body {
-    padding-top: 2em;
-    text-align: center;
-}
-
-.container {
-    padding: 2em 1em;
-    text-align: left;
-}
-
-.panel {
-    width: 30em;
-    margin: 1em auto;
-}
-
-input.color-control {
-    width: 5em;
-}
-
-div#color {
-    width: 5em;
-    height: 5em;
-
-    margin: 1em auto;
-}
-
-div.color-slider {
-    margin: 1em;
-}
-
-div.color-slider#slider-r .ui-slider-range {
-    background: #ff0000;
-}
-
-div.color-slider#slider-g .ui-slider-range {
-    background: #00ff00;
-}
-
-div.color-slider#slider-b .ui-slider-range {
-    background: #0000ff;
-}
-    """
-
     # test
     TITLE = u"DMX Control"
 
     def process (self) :
         if self.request.method == 'POST' :
-            self.color = tuple((int(x, 16) if x else 0) for x in (
+            # XXX
+            r, g, b = tuple((int(x, 16) if x else 0) for x in (
                     self.request.form.get('r'),
                     self.request.form.get('g'),
                     self.request.form.get('b'),
             ))
 
-            r, g, b = self.color
-
             self.app.dmx_color(r, g, b, 255)
 
-        else :
-            self.color = None
-
-        log.info("%s", self.color)
-
-    def render (self) :
-        if self.color :
-            r, g, b = self.color
+    def render_head (self, name, head) :
+        if head.alpha() is None :
+            head_input = head_slider = None
         else :
-            r = g = b = None
-
-        def color_swatch () :
-            return html.div(id='color',
-                    style='background-color: rgb({r}, {g}, {b})'.format(r=r, g=g, b=b)
-            )(' '),
-
-        def color_input (name, value) :
-            color = dict(r=0, g=0, b=0)
-            bgcolor = dict(r=0, g=0, b=0)
-
-            if value :
-                color[name] = value
-                alpha = value / 255.0
-            else :
-                alpha = 0
-
-            bgcolor[name] = 255
+            head_input = input(name, 'alpha', head.alpha()['alpha'])
+            head_slider = slider(name, 'alpha')
 
-            return html.input(type='text', name=name,
-                    class_      = 'form-control color-control',
-                    placeholder = name,
-                    id          = name,
+        rowspan = 1
 
-                    value   = '{v:02x}'.format(v=value) if value else None,
-                    style   = 'background-color: rgba({r}, {g}, {b}, {a:0.2f})'.format(a=alpha, **bgcolor),
+        if head.color() is None :
+            colors = { }
+            color = None
+        else :
+            colors = head.color()
+            color = head_color(name, colors)
+            rowspan += 3
+
+        yield html.tr(
+            html.th(rowspan=rowspan)(name),
+            html.td(head_input),
+            html.td(head_slider),
+            html.td(rowspan=rowspan)(color),
+        )
+                
+        for c in colors :
+            yield html.tr(
+                html.td(
+                    color_input(name, c, colors[c]),
+                ),
+                html.td(
+                    color_slider(name, c),
+                ),
             )
-
+    
+    def render (self) :
         return html.div(class_='container')(
-            html.div(class_='panel')(
-                color_swatch(),
-                html.div(id='slider-r', class_='color-slider')(' '),
-                html.div(id='slider-g', class_='color-slider')(' '),
-                html.div(id='slider-b', class_='color-slider')(' '),
-                html.form(action='.', method='POST', class_='form-inline')(
-                       #html.label(for_='color', class_='control-label')("Color"),
-                    html.div(class_='form-group')(
-                       color_input('r', r),
+            html.form(action='.', method='POST')(
+                html.table(class_='dmx')(
+                    html.thead(
+                        html.th(class_='dmx-head')(u"Head"),
+                        html.th(class_='dmx-value')(u"DMX"),
+                        html.th(class_='dmx-control')(u"Control"),
+                        html.th(class_='dmx-head-control')(u"Head Control"),
                     ),
-                    html.div(class_='form-group')(
-                       color_input('g', g),
-                    ),
-                    html.div(class_='form-group')(
-                       color_input('b', b),
-                    ),
-                    html.div(class_='form-group')(
-                        html.button(type='submit', class_='btn btn-primary')("Go"),
-                    ),
-                )
+                    html.tbody(self.render_head(name, head) for name, head in sorted(self.app.heads.iteritems())),
+                ),
+
+                html.button(type='submit', class_='btn btn-primary')("Go"),
             )
         )
 
@@ -145,17 +126,8 @@
         urls.rule('/',          Handler),
     ))
 
-    def __init__ (self, dmx, **opts) :
+    def __init__ (self, dmx, heads, **opts) :
         super(DMXWebApplication, self).__init__(**opts)
 
         self.dmx = dmx
-
-    def dmx_color (self, r, g, b, a=255) :
-        # Stairville LED Par56
-        self.dmx[1] = (0, r, g, b, 0)
-
-        # 4ch dimmer
-        self.dmx[5] = (a, a, a, a)
-
-        # American DJ - Mega Tri 60 - Mode 2
-        self.dmx[10] = (r, g, b, 0, a)
+        self.heads = heads
--- a/static/color-slider.js	Mon Apr 21 00:20:27 2014 +0300
+++ b/static/color-slider.js	Thu May 01 23:34:20 2014 +0300
@@ -6,59 +6,6 @@
         if (a == undefined)
             a = 1.0;
 
-        this.css('background', 'rgba(' + r + ', ' + g + ', ' + b + ', ' + a + ')');
+        this.css('background', 'rgba(' + (r * 255) + ', ' + (g * 255) + ', ' + (b * 255) + ', ' + a + ')');
     },
 });
-
-function color_slider_slide (event, ui) {
-    // $(this).css('background', 'rgba(0, 0, 0, ' + (ui.value / 255) + ')');
-
-    $('#color').background_color(
-            $('#slider-r').slider('value'),
-            $('#slider-g').slider('value'),
-            $('#slider-b').slider('value')
-    );
-    
-    $(['r', 'g', 'b']).each(function (i, c) {
-        var value = $('#slider-' + c).slider('value');
-        var input = $('#' + c);
-        var color = {r: 0, g: 0, b: 0};
-        
-        color[c] = 255;
-
-        input.val(value.toString(16));
-        input.background_color(color.r, color.g, color.b, value / 255);
-    });
-}
-
-function color_slider_input (input, c) {
-    var value;
-
-    if (input.val())
-        value = parseInt(input.val(), 16);
-    else
-        value = 0;
-    
-    $('#slider-' + c).slider('value', value);
-}
-
-$(function () {
-    $('.color-slider').slider({
-        orientation:    'horizontal',
-        range:          'min',
-        min:            0,
-        max:            255,
-
-        slide:          color_slider_slide,
-    });
-    
-    $(['r', 'g', 'b']).each(function (i, c) {
-        var input = $('#' + c);
-
-        // bind
-        input.change(function () { color_slider_input($(this), this.id); });
-
-        // initialize
-        color_slider_input(input, c);
-    });
-});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/static/dmx.css	Thu May 01 23:34:20 2014 +0300
@@ -0,0 +1,84 @@
+body {
+    padding-top: 2em;
+    text-align: center;
+}
+
+.container {
+    padding: 2em 1em;
+    text-align: left;
+}
+
+/*
+ * Table layout
+ */
+table.dmx {
+    margin: 1em auto;
+    width: 80%;
+
+    border-collapse: collapse;
+}
+
+table.dmx tr
+{
+    border: 1px dotted #aaa;
+}
+
+table.dmx th
+{
+    text-align: center;
+}
+
+th.dmx-head         { width: 5em; }
+th.dmx-value        { width: 5em; }
+th.dmx-control      { }
+th.dmx-head-control { width: 5em; }
+
+
+
+
+/* Controls */
+input.dmx-input {
+
+}
+
+div.dmx-color-background {
+    background-color: rgb(0, 0, 0);
+}
+
+div.dmx-color {
+    width: 5em;
+    height: 5em;
+
+    margin: 1em auto;
+
+    border: 1px dotted #aaa;
+}
+
+div.dmx-slider {
+    margin: 1em;
+}
+
+div.dmx-slider-alpha .ui-slider-range,
+div.dmx-slider-alpha .ui-slider-handle
+{
+    background: #ffffff;
+}
+
+div.dmx-slider-red .ui-slider-range,
+div.dmx-slider-red .ui-slider-handle
+{
+    background: #ff0000;
+}
+
+div.dmx-slider-green .ui-slider-range,
+div.dmx-slider-green .ui-slider-handle
+{
+    background: #00ff00;
+}
+
+div.dmx-slider-blue .ui-slider-range,
+div.dmx-slider-blue .ui-slider-handle
+{
+    background: #0000ff;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/static/dmx.js	Thu May 01 23:34:20 2014 +0300
@@ -0,0 +1,92 @@
+function dmx_input (head, attr) {
+    var value = $('.dmx-input#' + head + '-' + attr).val();
+    
+    if (value == undefined )
+        return undefined;
+
+    if (value == "")
+        return 0; // default
+    
+    return parseInt(value);
+}
+
+/*
+ * Update color for head.
+ */
+function dmx_color (head) {
+    var alpha = dmx_input(head, 'alpha');
+
+    if (alpha) 
+        alpha = alpha / 255;
+
+    $('.dmx-color#' + head + '-color').background_color(
+            dmx_input(head, 'red') / 255,
+            dmx_input(head, 'green') / 255,
+            dmx_input(head, 'blue') / 255,
+            alpha
+    );
+}
+
+/*
+ * Update slider from <input>.
+ */
+function _slider_input (input, slider) {
+    var value;
+
+    if (input.val())
+        value = parseInt(input.val());
+    else
+        value = 0;
+    
+    slider.slider('value', value);
+}
+
+/*
+ * Bind given <input> to given slider.
+ */
+function slider_input (input, slider) {
+    // bind
+    input.change(function () { _slider_input(input, slider); });
+
+    // initialize
+    _slider_input(input, slider);
+}
+
+$(function () {
+    $('.dmx-input').each(function () {
+        var attr = this.id;
+        var head = attr.split('-', 1)[0];
+        var input = $(this);
+        var slider = $('.dmx-slider#' + attr + '-slider');
+        var color = $('.dmx-color#' + head + '-color');
+        
+        // slider control
+        slider.slider({
+            orientation:    'horizontal',
+            range:          'min',
+            min:            0,
+            max:            255,
+
+            slide:          function () {
+                var value = slider.slider('value');
+                
+                // update input
+                input.val(value.toString());
+
+                if (color) {
+                    // update color value
+                    dmx_color(head);
+                }
+
+            },
+        });
+
+        // update slider from <input>
+        slider_input(input, slider);
+
+        // init
+        if (color) {
+            dmx_color(head);
+        }
+    });
+});