Changeset - d51014267bfd
[Not reviewed]
default
0 5 2
Drew Perttula - 9 years ago 2016-05-30 11:13:06
drewp@bigasterisk.com
move device-specific code out of collector. resolver isn't done yet. live.html can edit colors
Ignore-this: d537a0418ffde35b928387706977cbe0
7 files changed with 228 insertions and 75 deletions:
0 comments (0 inline, 0 general)
light9/collector/collector.py
Show inline comments
 
from __future__ import division
 
import time
 
import logging
 
from webcolors import hex_to_rgb
 
from light9.namespaces import L9, RDF, DEV
 
from light9.collector.output import setListElem
 
from light9.collector.device import toOutputAttrs
 

	
 
log = logging.getLogger('collector')
 

	
 
#class Device(object):
 
#    def setAttrs():
 
#        pass
 

	
 
def outputMap(graph, outputs):
 
    """From rdf config graph, compute a map of
 
       (device, attr) : (output, index)
 
    that explains which output index to set for any device update.
 
    """
 
    ret = {}
 
@@ -52,18 +48,25 @@ class Collector(object):
 
        staleClients = []
 
        for c, (_, t, _) in self.lastRequest.iteritems():
 
            if t < now - self.clientTimeoutSec:
 
                staleClients.append(c)
 
        for c in staleClients:
 
            del self.lastRequest[c]
 

	
 
    def _deviceType(self, d):
 
        for t in self.config.objects(d, RDF.type):
 
            if t == L9['Device']:
 
                continue
 
            return t
 
        
 
    def setAttrs(self, client, clientSession, settings):
 
        """
 
        settings is a list of (device, attr, value). Interpret rgb colors,
 
        resolve conflicting values, and call
 
        Output.update/Output.flush to send the new outputs.
 
        settings is a list of (device, attr, value). These attrs are
 
        device attrs. We resolve conflicting values, process them into
 
        output attrs, and call Output.update/Output.flush to send the
 
        new outputs.
 

	
 
        Call with settings=[] to ping us that your session isn't dead.
 
        """
 
        now = time.time()
 

	
 
        self._forgetStaleClients(now)
 
@@ -74,34 +77,35 @@ class Collector(object):
 
                prevClientSettings = {}
 
        else:
 
            prevClientSettings = {}
 
        for d, a, v in settings:
 
            prevClientSettings[(d, a)] = v
 
        self.lastRequest[client] = (clientSession, now, prevClientSettings)
 

	
 

	
 
        deviceAttrs = {} # device: {attr: value}
 
        for _, _, settings in self.lastRequest.itervalues():
 
            for (device, attr), value in settings.iteritems():
 
                # resolving conflicts goes around here
 
                deviceAttrs.setdefault(device, {})[attr] = value
 

	
 
        outputAttrs = {} # device: {attr: value}
 
        for d in deviceAttrs:
 
            outputAttrs[d] = toOutputAttrs(self._deviceType(d), deviceAttrs[d])
 
        
 
        pendingOut = {} # output : values
 

	
 
        # device always wants this
 
        self.setAttr(DEV['colorStrip'], L9['mode'], 215/255, pendingOut)
 
        
 
        for _, _, settings in self.lastRequest.itervalues():
 
            for (device, attr), value in settings.iteritems():
 
        for device, attrs in outputAttrs.iteritems():
 
            for attr, value in attrs.iteritems():
 
                self.setAttr(device, attr, value, pendingOut)
 

	
 
        self.flush(pendingOut)
 

	
 
    def setAttr(self, device, attr, value, pendingOut):
 
        if attr == L9['color']:
 
            [self.setAttr(device, a, x / 255, pendingOut) for a, x in zip(
 
                [L9['red'], L9['green'], L9['blue']],
 
                hex_to_rgb(value))]
 
            return
 
            
 
        output, index = self.outputMap[(device, attr)]
 
        outList = pendingOut.setdefault(output, [])
 
        setListElem(outList, index, int(float(value) * 255), combine=max)
 
        setListElem(outList, index, value, combine=max)
 

	
 
    def flush(self, pendingOut):
 
        """write any changed outputs"""
 
        for out, vals in pendingOut.iteritems():
 
            out.update(vals)
 
            out.flush()
light9/collector/collector_test.py
Show inline comments
 
@@ -25,13 +25,13 @@ class MockOutput(object):
 
    def __init__(self, connections):
 
        self.connections = connections
 
        self.updates = []
 

	
 
    def allConnections(self):
 
        return self.connections
 
    
 

	
 
    def update(self, values):
 
        self.updates.append(values)
 

	
 
    def flush(self):
 
        self.updates.append('flush')
 

	
 
@@ -60,128 +60,123 @@ class TestOutputMap(unittest.TestCase):
 
        out0 = MockOutput([(0, DMX0['c1'])])
 
        self.assertRaises(ValueError, outputMap, fromN3('''
 
          dmx0:c1 :connectedTo dev:inst1Brightness .
 
          dmx0:c2 :connectedTo dev:inst1Brightness .
 
          dev:inst1 a :Device; :brightness dev:inst1Brightness .
 
        '''), [out0])
 
        
 
        
 
        
 

	
 

	
 

	
 
class TestCollector(unittest.TestCase):
 
    def setUp(self):
 
        self.config = fromN3('''
 
        
 

	
 
        udmx:c1 :connectedTo dev:colorStripRed .
 
        udmx:c2 :connectedTo dev:colorStripGreen .
 
        udmx:c3 :connectedTo dev:colorStripBlue .
 
        udmx:c4 :connectedTo dev:colorStripMode .
 

	
 
        dev:colorStrip a :Device;
 
        dev:colorStrip a :Device, :ChauvetColorStrip;
 
          :red dev:colorStripRed;
 
          :green dev:colorStripGreen;
 
          :blue dev:colorStripBlue;
 
          :mode dev:colorStripMode .
 

	
 
        dmx0:c1 :connectedTo dev:inst1Brightness .
 
        dev:inst1 a :Device;
 
        dev:inst1 a :Device, :Dimmer;
 
          :brightness dev:inst1Brightness .
 
        ''')
 

	
 
        self.dmx0 = MockOutput([(0, DMX0['c1'])])
 
        self.udmx = MockOutput([(0, UDMX['c1']),
 
                                (1, UDMX['c2']),
 
                                (2, UDMX['c3']),
 
                                (3, UDMX['c4'])])
 

	
 
    def testRoutesSimpleOutput(self):
 
        c = Collector(self.config, outputs=[self.dmx0, self.udmx])
 

	
 
        c.setAttrs('client', 'sess1',
 
                   [(DEV['colorStrip'], L9['green'], Literal(1.0))])
 

	
 
        self.assertEqual([[0, 255, 0, 215], 'flush'], self.udmx.updates)
 
        self.assertEqual([], self.dmx0.updates)
 

	
 
    def testRoutesColorOutput(self):
 
        c = Collector(self.config, outputs=[self.dmx0, self.udmx])
 

	
 
        c.setAttrs('client', 'sess1',
 
                   [(DEV['colorStrip'], L9['color'], Literal('#ff0000'))])
 
                   [(DEV['colorStrip'], L9['color'], '#00ff00')])
 

	
 
        self.assertEqual([[255, 0, 0, 215], 'flush'], self.udmx.updates)
 
        self.assertEqual([[0, 255, 0, 215], 'flush'], self.udmx.updates)
 
        self.assertEqual([], self.dmx0.updates)
 

	
 
    def testOutputMaxOfTwoClients(self):
 
        c = Collector(self.config, outputs=[self.dmx0, self.udmx])
 

	
 
        c.setAttrs('client1', 'sess1',
 
                   [(DEV['colorStrip'], L9['color'], Literal('#ff0000'))])
 
                   [(DEV['colorStrip'], L9['color'], '#ff0000')])
 
        c.setAttrs('client2', 'sess1',
 
                   [(DEV['colorStrip'], L9['color'], Literal('#333333'))])
 
                   [(DEV['colorStrip'], L9['color'], '#333333')])
 

	
 
        self.assertEqual([[255, 0, 0, 215], 'flush',
 
                          [255, 51, 51, 215], 'flush'],
 
                         self.udmx.updates)
 
        self.assertEqual([], self.dmx0.updates)
 
        
 

	
 
    def testClientOnSameOutputIsRememberedOverCalls(self):
 
        c = Collector(self.config, outputs=[self.dmx0, self.udmx])
 

	
 
        c.setAttrs('client1', 'sess1', [(DEV['colorStrip'], L9['red'], .8)])
 
        c.setAttrs('client2', 'sess1', [(DEV['colorStrip'], L9['red'], .6)])
 
        c.setAttrs('client1', 'sess1', [(DEV['colorStrip'], L9['red'], .5)])
 
        
 
        self.assertEqual([[204, 0, 0, 215], 'flush',
 
                          [204, 0, 0, 215], 'flush',
 
                          [153, 0, 0, 215], 'flush'],
 
        c.setAttrs('client1', 'sess1',
 
                   [(DEV['colorStrip'], L9['color'], '#080000')])
 
        c.setAttrs('client2', 'sess1',
 
                   [(DEV['colorStrip'], L9['color'], '#060000')])
 
        c.setAttrs('client1', 'sess1',
 
                   [(DEV['colorStrip'], L9['color'], '#050000')])
 

	
 
        self.assertEqual([[8, 0, 0, 215], 'flush',
 
                          [8, 0, 0, 215], 'flush',
 
                          [6, 0, 0, 215], 'flush'],
 
                         self.udmx.updates)
 
        self.assertEqual([], self.dmx0.updates)
 
        
 

	
 
    def testClientsOnDifferentOutputs(self):
 
        c = Collector(self.config, outputs=[self.dmx0, self.udmx])
 

	
 
        c.setAttrs('client1', 'sess1', [(DEV['colorStrip'], L9['red'], .8)])
 
        c.setAttrs('client1', 'sess1', [(DEV['colorStrip'], L9['color'], '#aa0000')])
 
        c.setAttrs('client2', 'sess1', [(DEV['inst1'], L9['brightness'], .5)])
 

	
 
        # ok that udmx is flushed twice- it can screen out its own duplicates
 
        self.assertEqual([[204, 0, 0, 215], 'flush',
 
                          [204, 0, 0, 215], 'flush'], self.udmx.updates)
 
        self.assertEqual([[170, 0, 0, 215], 'flush',
 
                          [170, 0, 0, 215], 'flush'], self.udmx.updates)
 
        self.assertEqual([[127], 'flush'], self.dmx0.updates)
 
        
 

	
 
    def testNewSessionReplacesPreviousOutput(self):
 
        # ..as opposed to getting max'd with it
 
        c = Collector(self.config, outputs=[self.dmx0, self.udmx])
 

	
 
        c.setAttrs('client1', 'sess1', [(DEV['inst1'], L9['brightness'], .8)])
 
        c.setAttrs('client1', 'sess2', [(DEV['inst1'], L9['brightness'], .5)])
 
        
 

	
 
        self.assertEqual([[204], 'flush', [127], 'flush'], self.dmx0.updates)
 

	
 
    def testNewSessionDropsPreviousSettingsOfOtherAttrs(self):
 
        c = Collector(self.config, outputs=[self.dmx0, self.udmx])
 

	
 
        c.setAttrs('client1', 'sess1', [(DEV['colorStrip'], L9['red'], 1)])
 
        c.setAttrs('client1', 'sess2', [(DEV['colorStrip'], L9['green'], 1)])
 
        
 

	
 
        self.assertEqual([[255, 0, 0, 215], 'flush',
 
                          [0, 255, 0, 215], 'flush'], self.udmx.updates)
 
        
 

	
 
    def testClientIsForgottenAfterAWhile(self):
 
        with freeze_time(datetime.datetime.now()) as ft:
 
            c = Collector(self.config, outputs=[self.dmx0, self.udmx])
 
            c.setAttrs('cli1', 'sess1', [(DEV['inst1'], L9['brightness'], .5)])
 
            ft.tick(delta=datetime.timedelta(seconds=1))
 
            c.setAttrs('cli2', 'sess1', [(DEV['inst1'], L9['brightness'], .2)])
 
            ft.tick(delta=datetime.timedelta(seconds=9.1))
 
            c.setAttrs('cli2', 'sess1', [(DEV['inst1'], L9['brightness'], .4)])
 
            self.assertEqual([[127], 'flush', [127], 'flush', [102], 'flush'],
 
                             self.dmx0.updates)
 

	
 
    def testClientUpdatesAreCollected(self):
 
        # second call to setAttrs doesn't forget the first
 
        c = Collector(self.config, outputs=[self.dmx0, self.udmx])
 

	
 
        c.setAttrs('client1', 'sess1', [(DEV['colorStrip'], L9['red'], 1)])
 
        c.setAttrs('client1', 'sess1', [(DEV['colorStrip'], L9['green'], 1)])
 
        
 
        self.assertEqual([[255, 0, 0, 215], 'flush',
 
                          [255, 255, 0, 215], 'flush'], self.udmx.updates)
 
        self.assertEqual([], self.dmx0.updates)
 
        c.setAttrs('client1', 'sess1', [(DEV['inst1'], L9['brightness'], .5)])
 
        c.setAttrs('client1', 'sess1', [(DEV['inst1'], L9['brightness'], 1)])
 
        c.setAttrs('client1', 'sess1', [(DEV['colorStrip'], L9['color'], '#00ff00')])
 

	
 
        self.assertEqual([[0, 255, 0, 215], 'flush'], self.udmx.updates)
 
        self.assertEqual([[127], 'flush', [255], 'flush', [255], 'flush'], self.dmx0.updates)
light9/collector/device.py
Show inline comments
 
new file 100644
 
from __future__ import division
 
import logging
 
import math
 
from light9.namespaces import L9, RDF, DEV
 
from webcolors import hex_to_rgb
 

	
 
log = logging.getLogger('device')
 

	
 
class Device(object):
 
    def setAttrs():
 
        pass
 

	
 

	
 
class ChauvetColorStrip(Device):
 
    """
 
     device attrs:
 
       color
 
    """
 
        
 
class Mini15(Device):
 
    """
 
    plan:
 

	
 
      device attrs
 
        rx, ry
 
        color
 
        gobo
 
        goboShake
 
        imageAim (configured with a file of calibration data)
 
    """
 

	
 
def _8bit(f):
 
    return min(255, max(0, int(f * 255)))
 

	
 
def resolve(deviceType, deviceAttr, values):
 
    """
 
    return one value to use for this attr, given a set of them that
 
    have come in simultaneously
 
    """
 
    raise NotImplementedError
 
    
 
def toOutputAttrs(deviceType, deviceAttrSettings):
 
    """
 
    Given settings like {L9['color']: Literal('#ff0000')}, return a
 
    similar dict where the keys are output attrs and the values are
 
    suitable for Collector.setAttr
 
    """
 
    if deviceType == L9['ChauvetColorStrip']:
 
        color = deviceAttrSettings.get(L9['color'], '#000000')
 
        r, g, b = hex_to_rgb(color)
 
        return {
 
            L9['mode']: 215,
 
            L9['red']: r,
 
            L9['green']: g,
 
            L9['blue']: b
 
            }
 
    elif deviceType == L9['Dimmer']:
 
        return {L9['brightness']: _8bit(deviceAttrSettings.get(L9['brightness'], 0))}
 
    elif deviceType == L9['Mini15']:
 
        inp = deviceAttrSettings
 
        rx8 = float(inp.get(L9['rx'], 0)) / 540 * 255
 
        ry8 = float(inp.get(L9['ry'], 0)) / 240 * 255
 
        r, g, b = hex_to_rgb(inp.get(L9['color'], '#000000'))
 

	
 
        return {
 
            L9['xRotation']: int(math.floor(rx8)),
 
            # didn't find docs on this, but from tests it looks like 64 fine steps takes you to the next coarse step
 
            L9['xFine']: _8bit((rx8 % 1.0) / 4),
 
            L9['yRotation']: int(math.floor(ry8)),
 
            L9['yFine']: _8bit((ry8 % 1.0) / 4),
 
            L9['rotationSpeed']: 0,
 
            L9['dimmer']: 255,
 
            L9['red']: r,
 
            L9['green']: g,
 
            L9['blue']: b,
 
            L9['colorChange']: 0,
 
            L9['colorSpeed']: 0,
 
            L9['goboShake']: 0,
 
            L9['goboChoose']: 0,
 
        }
 
    else:
 
        raise NotImplementedError('device %r' % deviceType)
light9/collector/device_test.py
Show inline comments
 
new file 100644
 
import unittest
 
from rdflib import Literal
 
from light9.namespaces import L9
 

	
 
from light9.collector.device import toOutputAttrs
 

	
 
class TestColorStrip(unittest.TestCase):
 
    def testConvertDeviceToOutputAttrs(self):
 
        out = toOutputAttrs(L9['ChauvetColorStrip'],
 
                            {L9['color']: Literal('#ff0000')})
 
        self.assertEqual({L9['mode']: 215,
 
                          L9['red']: 255,
 
                          L9['green']: 0,
 
                          L9['blue']: 0
 
                      }, out)
 
        
 
class TestDimmer(unittest.TestCase):
 
    def testConvert(self):
 
        self.assertEqual({L9['brightness']: 127},
 
                         toOutputAttrs(L9['Dimmer'], {L9['brightness']: .5}))
 

	
 
class TestMini15(unittest.TestCase):
 
    def testConvertColor(self):
 
        out = toOutputAttrs(L9['Mini15'], {L9['color']: '#010203'})
 
        self.assertEqual(255, out[L9['dimmer']])
 
        self.assertEqual(1, out[L9['red']])
 
        self.assertEqual(2, out[L9['green']])
 
        self.assertEqual(3, out[L9['blue']])
 
    def testConvertRotation(self):
 
        out = toOutputAttrs(L9['Mini15'], {L9['rx']: Literal(90), L9['ry']: Literal(45)})
 
        self.assertEqual(42, out[L9['xRotation']])
 
        self.assertEqual(31, out[L9['xFine']])
 
        self.assertEqual(47, out[L9['yRotation']])
 
        self.assertEqual(51, out[L9['yFine']])
 
        self.assertEqual(0, out[L9['rotationSpeed']])
light9/web/lib/bower.json
Show inline comments
 
@@ -6,8 +6,12 @@
 
    "iron-ajax": "PolymerElements/iron-ajax#~1.2.0",
 
    "jquery": "~2.2.4",
 
    "underscore": "~1.8.3",
 
    "jquery-ui": "~1.11.4",
 
    "QueryString": "http://unixpapa.com/js/QueryString.js",
 
    "knockout": "~3.4.0"
 
  },
 
  "resolutions": {
 
    "paper-styles": "^1.1.4",
 
    "web-animations-js": "^2.2.0"
 
  }
 
}
light9/web/live.html
Show inline comments
 
@@ -11,30 +11,49 @@
 
  </head>
 
  <body>
 
    live
 
    <dom-module id="light9-live-control">
 
      <template>
 
        <style>
 
         paper-slider { width: 100%; }
 
        </style>
 
        <iron-ajax url="/collector/attrs" method="PUT" id="put"></iron-ajax>
 
        <paper-slider min="0" max="1" step=".01" editable content-type="application/json" immediate-value="{{v1}}"></paper-slider>
 

	
 
        <template is="dom-if" if="{{useSlider}}">
 
          <paper-slider min="0"
 
                        max="{{max}}"
 
                        step=".01"
 
                        editable
 
                        content-type="application/json"
 
                        immediate-value="{{v1}}"></paper-slider>
 
        </template>
 
        <template is="dom-if" if="{{useColor}}">
 
          <input type="color" id="col" on-input="onPickedColor" value="{{pickedColor}}">
 
        </template>
 
      </template>
 
      <script>
 
       HTMLImports.whenReady(function () {
 
           Polymer({
 
               is: "light9-live-control",
 
               properties: {
 
                   device: {type: String},
 
                   attr: {type: String},
 
                   max: {type: Number, value: 1},
 
                   v1: {type: Number, notify: true, observer: "ch"},
 
                   useSlider: {type: Boolean, computed: '_useSlider(attr)'},
 
                   useColor: {type: Boolean, computed: '_useColor(attr)'},
 
                   pickedColor: {observer: 'onPickedColor'},
 
               },
 
               ready: function() {
 
                   // only need 1 ping for the clientsession, not one per
 
                   // control.
 
                   setInterval(this.ping.bind(this), 9000);
 
               },
 
               onPickedColor: function(ev) {
 
                   this.ch(ev.target.value);
 
               },
 
               ch: function(lev) {
 
                   this.$.put.body = JSON.stringify({
 
                       "settings":[
 
                           [this.device, this.attr, lev]],
 
                       "client":"c",
 
                       "clientSession":"cs"});
 
@@ -43,25 +62,39 @@
 
               ping: function() {
 
                   this.$.put.body = JSON.stringify({
 
                       "settings":[],
 
                       "client":"c",
 
                       "clientSession":"cs"});
 
                   this.$.put.generateRequest();
 
               }
 
               },
 
               _useSlider: function(attr) {
 
                   return attr != 'http://light9.bigasterisk.com/color';
 
               },
 
               _useColor: function(attr) {
 
                   return attr == 'http://light9.bigasterisk.com/color';
 
               },
 
           });
 
       });
 
      </script>
 
    </dom-module>
 
    <light9-live-control
 
        device="http://light9.bigasterisk.com/device/colorStrip"
 
        attr="http://light9.bigasterisk.com/red"
 
        attr="http://light9.bigasterisk.com/color"
 
    ></light9-live-control>
 
    <light9-live-control
 
        device="http://light9.bigasterisk.com/device/colorStrip"
 
        attr="http://light9.bigasterisk.com/green"
 
    ></light9-live-control>
 
    <light9-live-control
 
        device="http://light9.bigasterisk.com/device/colorStrip"
 
        attr="http://light9.bigasterisk.com/blue"
 
    ></light9-live-control>
 
    <hr>
 
    <h2>moving</h2>
 
    rx <light9-live-control
 
           device="http://light9.bigasterisk.com/device/moving1"
 
           attr="http://light9.bigasterisk.com/rx" max="540"
 
       ></light9-live-control>
 
    ry <light9-live-control
 
           device="http://light9.bigasterisk.com/device/moving1"
 
           attr="http://light9.bigasterisk.com/ry" max="240"
 
       ></light9-live-control>
 
    color    <light9-live-control
 
                 device="http://light9.bigasterisk.com/device/moving1"
 
                 attr="http://light9.bigasterisk.com/color"
 
             ></light9-live-control>
 
    
 
    
 
  </body>
 
</html>
makefile
Show inline comments
 
NOSEARGS="--no-path-adjustment light9.rdfdb.rdflibpatch light9.rdfdb.patch light9.effecteval.test_effect light9.collector.collector_test light9.collector.output_test"
 
NOSEARGS="--no-path-adjustment light9.rdfdb.rdflibpatch light9.rdfdb.patch light9.effecteval.test_effect light9.collector"
 

	
 
tests:
 
	eval env/bin/nosetests -x $(NOSEARGS)
 

	
 
tests_watch:
 
	eval env/bin/nosetests --with-watcher $(NOSEARGS)
0 comments (0 inline, 0 general)