# HG changeset patch # User Tero Marttila # Date 1398976460 -10800 # Node ID 136e210fce82b0b437be696b920eac590285cc00 # Parent b5878197d0179b1df64b1dd9946cc0564a80adb9 qmsk.dmx: new Head-based model/view; output/updates not yet implemented diff -r b5878197d017 -r 136e210fce82 bin/dmx-web.py --- 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) diff -r b5878197d017 -r 136e210fce82 qmsk/dmx/control.py --- 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 diff -r b5878197d017 -r 136e210fce82 qmsk/dmx/heads.py --- /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', + ] + diff -r b5878197d017 -r 136e210fce82 qmsk/dmx/web.py --- 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 diff -r b5878197d017 -r 136e210fce82 static/color-slider.js --- 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); - }); -}); diff -r b5878197d017 -r 136e210fce82 static/dmx.css --- /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; +} + diff -r b5878197d017 -r 136e210fce82 static/dmx.js --- /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 . + */ +function _slider_input (input, slider) { + var value; + + if (input.val()) + value = parseInt(input.val()); + else + value = 0; + + slider.slider('value', value); +} + +/* + * Bind given 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 + slider_input(input, slider); + + // init + if (color) { + dmx_color(head); + } + }); +});