Changeset - 37cbb245d93c
[Not reviewed]
default
0 10 0
Drew Perttula - 8 years ago 2017-03-28 07:48:10
drewp@bigasterisk.com
fix tests. add logging, some mypy types.
Ignore-this: 61371f65438a4e77f70d21cc5d5193bf
10 files changed with 164 insertions and 152 deletions:
0 comments (0 inline, 0 general)
light9/collector/collector.py
Show inline comments
 
from __future__ import division
 
import time
 
import logging
 
from rdflib import Literal
 
from light9.namespaces import L9, RDF, DEV
 
from light9.namespaces import L9, RDF
 
from light9.collector.output import setListElem
 
from light9.collector.device import toOutputAttrs, resolve
 

	
 
# types only
 
from rdflib import Graph, URIRef
 
from typing import List, Dict, Tuple, Any, TypeVar, Generic
 
from light9.collector.output import Output
 

	
 
ClientType = TypeVar('ClientType')
 
ClientSessionType = TypeVar('ClientSessionType')
 

	
 
log = logging.getLogger('collector')
 

	
 
def outputMap(graph, outputs):
 
    # type: (Graph, List[Output]) -> Dict[Tuple[URIRef, URIRef], Tuple[Output, int]]
 
    """From rdf config graph, compute a map of
 
       (device, outputattr) : (output, index)
 
    that explains which output index to set for any device update.
 
    """
 
    ret = {}
 

	
 
    outputByUri = {}  # universeUri : output
 
    for out in outputs:
 
        outputByUri[out.uri] = out
 

	
 
    for dc in graph.subjects(RDF.type, L9['DeviceClass']):
 
        log.info('mapping DeviceClass %s', dc)
 
        for dev in graph.subjects(RDF.type, dc):
 
            output = outputByUri[graph.value(dev, L9['dmxUniverse'])]
 
            log.info('  mapping device %s', dev)
 
            universe = graph.value(dev, L9['dmxUniverse'])
 
            try:
 
                output = outputByUri[universe]
 
            except Exception:
 
                log.warn('dev %r :dmxUniverse %r', dev, universe)
 
                raise
 
            dmxBase = int(graph.value(dev, L9['dmxBase']).toPython())
 
            for row in graph.objects(dc, L9['attr']):
 
                outputAttr = graph.value(row, L9['outputAttr'])
 
                offset = int(graph.value(row, L9['dmxOffset']).toPython())
 
                index = dmxBase + offset - 1
 
                ret[(dev, outputAttr)] = (output, index)
 
                log.info('map %s,%s to %s,%s', dev, outputAttr, output, index)
 
                log.info('    map %s to %s,%s', outputAttr, output, index)
 
    return ret
 
        
 
class Collector(object):
 
class Collector(Generic[ClientType, ClientSessionType]):
 
    def __init__(self, graph, outputs, clientTimeoutSec=10):
 
        # type: (Graph, List[Output], float) -> None
 
        self.graph = graph
 
        self.outputs = outputs
 
        self.clientTimeoutSec = clientTimeoutSec
 

	
 
        self.graph.addHandler(self.rebuildOutputMap)
 
        self.lastRequest = {} # client : (session, time, {(dev,devattr): latestValue})
 
        self.stickyAttrs = {} # (dev, devattr): value to use instead of 0
 

	
 
        # client : (session, time, {(dev,devattr): latestValue})
 
        self.lastRequest = {} # type: Dict[ClientType, Tuple[ClientSessionType, float, Dict[Tuple[URIRef, URIRef], float]]]
 

	
 
        # (dev, devAttr): value to use instead of 0
 
        self.stickyAttrs = {} # type: Dict[Tuple[URIRef, URIRef], float] 
 

	
 
    def rebuildOutputMap(self):
 
        self.outputMap = outputMap(self.graph, self.outputs) # (device, outputattr) : (output, index)
 
        self.deviceType = {} # uri: type that's a subclass of Device
 
        self.remapOut = {} # (device, deviceAttr) : (start, end)
 
        for dc in self.graph.subjects(RDF.type, L9['DeviceClass']):
 
            for dev in self.graph.subjects(RDF.type, dc):
 
                self.deviceType[dev] = dc
 

	
 
                for remap in self.graph.objects(dev, L9['outputAttrRange']):
 
                    attr = self.graph.value(remap, L9['outputAttr'])
 
                    start = float(self.graph.value(remap, L9['start']))
 
                    end = float(self.graph.value(remap, L9['end']))
 
                    self.remapOut[(dev, attr)] = start, end
 

	
 
    def _forgetStaleClients(self, now):
 
        # type: (float) -> None
 
        staleClients = []
 
        for c, (_, t, _) in self.lastRequest.iteritems():
 
        for c, (_, t, _2) in self.lastRequest.iteritems():
 
            if t < now - self.clientTimeoutSec:
 
                staleClients.append(c)
 
        for c in staleClients:
 
            log.info('forgetting stale client %r', c)
 
            del self.lastRequest[c]
 

	
 
    def resolvedSettingsDict(self, settingsList):
 
        out = {}
 
        # type: (List[Tuple[URIRef, URIRef, float]]) -> Dict[Tuple[URIRef, URIRef], float]
 
        out = {} # type: Dict[Tuple[URIRef, URIRef], float]
 
        for d, da, v in settingsList:
 
            if (d, da) in out:
 
                out[(d, da)] = resolve(d, da, [out[(d, da)], v])
 
            else:
 
                out[(d, da)] = v
 
        return out
 

	
 
    def _warnOnLateRequests(self, client, now, sendTime):
 
        requestLag = now - sendTime
 
        if requestLag > .1:
 
            log.warn('collector.setAttrs from %s is running %.1fms after the request was made',
 
                     client, requestLag * 1000)
 
        
 
    def setAttrs(self, client, clientSession, settings, sendTime):
 
        """
 
        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()
 
        requestLag = now - sendTime
 
        if requestLag > .1:
 
            log.warn('collector.setAttrs from %s is running %.1fms after the request was made',
 
                     client, requestLag * 1000)
 
        self._warnOnLateRequests(client, now, sendTime)
 

	
 
        self._forgetStaleClients(now)
 

	
 
        uniqueSettings = self.resolvedSettingsDict(settings)
 
        self.lastRequest[client] = (clientSession, now, uniqueSettings)
 

	
 
        deviceAttrs = {} # device: {deviceAttr: value}       
 
        for _, _, lastSettings in self.lastRequest.itervalues():
 
            for (device, deviceAttr), value in lastSettings.iteritems():
 
                if (device, deviceAttr) in self.remapOut:
 
                    start, end = self.remapOut[(device, deviceAttr)]
 
                    value = Literal(start + float(value) * (end - start))
 

	
 
                attrs = deviceAttrs.setdefault(device, {})
 
                if deviceAttr in attrs:
 
                    value = resolve(device, deviceAttr, [attrs[deviceAttr], value])
 
                attrs[deviceAttr] = value
 
                # list should come from the graph. these are attrs
 
                # that should default to holding the last position,
 
                # not going to 0.
 
                if deviceAttr in [L9['rx'], L9['ry'], L9['zoom'], L9['focus']]:
 
                    self.stickyAttrs[(device, deviceAttr)] = value
 

	
 
        
 
        # e.g. don't let an unspecified rotation go to 0
 
        for (d, da), v in self.stickyAttrs.iteritems():
 
            daDict = deviceAttrs.setdefault(d, {})
 
            if da not in daDict:
 
                daDict[da] = v
 
                    
 
        outputAttrs = {} # device: {outputAttr: value}
 
        for d in deviceAttrs:
 
            try:
 
                devType = self.deviceType[d]
 
            except KeyError:
 
                log.warn("request for output to unconfigured device %s" % d)
 
                continue
 
            outputAttrs[d] = toOutputAttrs(devType, deviceAttrs[d])
 
        
 
        pendingOut = {} # output : values
 
        for out in self.outputs:
 
            pendingOut[out] = [0] * out.numChannels
 

	
 
        for device, attrs in outputAttrs.iteritems():
 
            for outputAttr, value in attrs.iteritems():
 
                self.setAttr(device, outputAttr, value, pendingOut)
 

	
 
        dt1 = 1000 * (time.time() - now)
 
        self.flush(pendingOut)
 
        dt2 = 1000 * (time.time() - now)
 
        if dt1 > 10:
 
            print "slow setAttrs: %.1fms -> flush -> %.1fms. lr %s da %s oa %s" % (
 
                dt1, dt2, len(self.lastRequest), len(deviceAttrs), len(outputAttrs)
 
            )
 

	
 
    def setAttr(self, device, outputAttr, value, pendingOut):
 
        output, index = self.outputMap[(device, outputAttr)]
 
        outList = pendingOut[output]
 
        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
 
import unittest
 
import datetime
 
import datetime, time
 
from freezegun import freeze_time
 
from rdflib import Namespace
 
from rdflib import Namespace, URIRef
 

	
 
from light9.namespaces import L9, DEV
 
from light9.collector.collector import Collector, outputMap
 
from light9.rdfdb.mock_syncedgraph import MockSyncedGraph
 

	
 
UDMX = Namespace('http://light9.bigasterisk.com/output/udmx/')
 
DMX0 = Namespace('http://light9.bigasterisk.com/output/dmx0/')
 

	
 
PREFIX = '''
 
   @prefix : <http://light9.bigasterisk.com/> .
 
        @prefix dev: <http://light9.bigasterisk.com/device/> .
 
        @prefix udmx: <http://light9.bigasterisk.com/output/udmx/> .
 
        @prefix dmx0: <http://light9.bigasterisk.com/output/dmx0/> .
 
'''
 

	
 
THEATER = '''
 
        :brightness         a :DeviceAttr; :dataType :scalar .
 

	
 
        :SimpleDimmer a :DeviceClass;
 
          :deviceAttr :brightness;
 
          :attr
 
            [ :outputAttr :level; :dmxOffset 0 ] .
 
                
 
        :ChauvetColorStrip a :DeviceClass;
 
          :deviceAttr :color;
 
          :attr
 
            [ :outputAttr :mode;  :dmxOffset 0 ],
 
            [ :outputAttr :red;   :dmxOffset 1 ],
 
            [ :outputAttr :green; :dmxOffset 2 ],
 
            [ :outputAttr :blue;  :dmxOffset 3 ] .
 

	
 
'''
 

	
 
t0 = 0 # time
 

	
 
class MockOutput(object):
 
    def __init__(self, connections):
 
    def __init__(self, uri, connections):
 
        self.connections = connections
 
        self.updates = []
 
        self.uri = uri
 
        self.numChannels = 4
 

	
 
    def allConnections(self):
 
        return self.connections
 

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

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

	
 
@unittest.skip("outputMap got rewritten and mostly doesn't raise on these cases")
 
class TestOutputMap(unittest.TestCase):
 
    def testWorking(self):
 
        out0 = MockOutput([(0, DMX0['c1'])])
 
        out0 = MockOutput(UDMX, [(0, DMX0['c1'])])
 
        m = outputMap(MockSyncedGraph(PREFIX + '''
 
          dmx0:c1 :connectedTo dev:inst1Brightness .
 
          dev:inst1 a :Device; :brightness dev:inst1Brightness .
 
        '''), [out0])
 
        self.assertEqual({(DEV['inst1'], L9['brightness']): (out0, 0)}, m)
 
        
 
    def testMissingOutput(self):
 
        out0 = MockOutput([(0, DMX0['c1'])])
 
        out0 = MockOutput(UDMX, [(0, DMX0['c1'])])
 
        self.assertRaises(KeyError, outputMap, MockSyncedGraph(PREFIX + '''
 
          dmx0:c2 :connectedTo dev:inst1Brightness .
 
          dev:inst1 a :Device; :brightness dev:inst1Brightness .
 
        '''), [out0])
 

	
 
    def testMissingOutputConnection(self):
 
        out0 = MockOutput([(0, DMX0['c1'])])
 
        out0 = MockOutput(UDMX, [(0, DMX0['c1'])])
 
        self.assertRaises(ValueError, outputMap, MockSyncedGraph(PREFIX + '''
 
          dev:inst1 a :Device; :brightness dev:inst1Brightness .
 
        '''), [out0])
 

	
 
    def testMultipleOutputConnections(self):
 
        out0 = MockOutput([(0, DMX0['c1'])])
 
        out0 = MockOutput(UDMX, [(0, DMX0['c1'])])
 
        self.assertRaises(ValueError, outputMap, MockSyncedGraph(PREFIX + '''
 
          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 = MockSyncedGraph(PREFIX + '''
 

	
 
        udmx:c1 :connectedTo dev:colorStripRed .
 
        udmx:c2 :connectedTo dev:colorStripGreen .
 
        udmx:c3 :connectedTo dev:colorStripBlue .
 
        udmx:c4 :connectedTo dev:colorStripMode .
 
        self.config = MockSyncedGraph(PREFIX + THEATER + '''
 

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

	
 
        dmx0:c1 :connectedTo dev:inst1Brightness .
 
        dev:inst1 a :Device, :Dimmer;
 
          :brightness dev:inst1Brightness .
 
        dev:inst1 a :Device, :SimpleDimmer;
 
          :dmxUniverse dmx0:; :dmxBase 1;
 
          :level dev:inst1Brightness .
 
        ''')
 

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

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

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

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

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

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

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

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

	
 
        c.setAttrs('client1', 'sess1',
 
                   [(DEV['colorStrip'], L9['color'], '#080000')])
 
                   [(DEV['colorStrip'], L9['color'], '#080000')], t0)
 
        c.setAttrs('client2', 'sess1',
 
                   [(DEV['colorStrip'], L9['color'], '#060000')])
 
                   [(DEV['colorStrip'], L9['color'], '#060000')], t0)
 
        c.setAttrs('client1', 'sess1',
 
                   [(DEV['colorStrip'], L9['color'], '#050000')])
 
                   [(DEV['colorStrip'], L9['color'], '#050000')], t0)
 

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

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

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

	
 
        # ok that udmx is flushed twice- it can screen out its own duplicates
 
        self.assertEqual([[170, 0, 0, 215], 'flush',
 
                          [170, 0, 0, 215], 'flush'], self.udmx.updates)
 
        self.assertEqual([[127], 'flush'], self.dmx0.updates)
 
        self.assertEqual([[215, 170, 0, 0], 'flush',
 
                          [215, 170, 0, 0], 'flush'], self.udmx.updates)
 
        self.assertEqual([[0, 0, 0, 0], 'flush',
 
                          [127, 0, 0, 0], '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)])
 
        c.setAttrs('client1', 'sess1', [(DEV['inst1'], L9['brightness'], .8)], t0)
 
        c.setAttrs('client1', 'sess2', [(DEV['inst1'], L9['brightness'], .5)], t0)
 

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

	
 
    def testNewSessionDropsPreviousSettingsOfOtherAttrs(self):
 
        
 
        c = Collector(MockSyncedGraph(PREFIX + '''
 

	
 
        udmx:c1 :connectedTo dev:colorStripRed .
 
        udmx:c2 :connectedTo dev:colorStripGreen .
 
        udmx:c3 :connectedTo dev:colorStripBlue .
 
        udmx:c4 :connectedTo dev:colorStripMode .
 
        c = Collector(MockSyncedGraph(PREFIX + THEATER + '''
 

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

	
 
        dmx0:c1 :connectedTo dev:inst1Brightness .
 
        dev:inst1 a :Device, :Dimmer;
 
          :brightness dev:inst1Brightness .
 
        dev:inst1 a :Device, :SimpleDimmer;
 
          :dmxUniverse dmx0:; :dmxBase 0;
 
          :level dev:inst1Brightness .
 
        '''), outputs=[self.dmx0, self.udmx])
 

	
 
        c.setAttrs('client1', 'sess1',
 
                   [(DEV['colorStrip'], L9['color'], '#ff0000')])
 
                   [(DEV['colorStrip'], L9['color'], '#ff0000')], t0)
 
        c.setAttrs('client1', 'sess2',
 
                   [(DEV['colorStrip'], L9['color'], '#00ff00')])
 
                   [(DEV['colorStrip'], L9['color'], '#00ff00')], t0)
 

	
 
        self.assertEqual([[255, 0, 0, 215], 'flush',
 
                          [0, 255, 0, 215], 'flush'], self.udmx.updates)
 
        self.assertEqual([[215, 255, 0, 0], 'flush',
 
                          [215, 0, 255, 0], '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)])
 
            c.setAttrs('cli1', 'sess1', [(DEV['inst1'], L9['brightness'], .5)],
 
                       time.time())
 
            ft.tick(delta=datetime.timedelta(seconds=1))
 
            c.setAttrs('cli2', 'sess1', [(DEV['inst1'], L9['brightness'], .2)])
 
            # this max's with cli1's value so we still see .5
 
            c.setAttrs('cli2', 'sess1', [(DEV['inst1'], L9['brightness'], .2)], 
 
                       time.time())
 
            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'],
 
            # now cli1 is forgotten, so our value appears
 
            c.setAttrs('cli2', 'sess1', [(DEV['inst1'], L9['brightness'], .4)], 
 
                       time.time())
 
            self.assertEqual([[127, 0, 0, 0], 'flush',
 
                              [127, 0, 0, 0], 'flush',
 
                              [102, 0, 0, 0], 'flush'],
 
                             self.dmx0.updates)
 

	
 
    def testClientUpdatesAreCollected(self):
 
        # second call to setAttrs doesn't forget the first
 
    def testClientUpdatesAreNotMerged(self):
 
        # second call to setAttrs forgets the first
 
        c = Collector(self.config, outputs=[self.dmx0, self.udmx])
 
        t0 = time.time()
 
        c.setAttrs('client1', 'sess1', [(DEV['inst1'], L9['brightness'], .5)], t0)
 
        c.setAttrs('client1', 'sess1', [(DEV['inst1'], L9['brightness'], 1)], t0)
 
        c.setAttrs('client1', 'sess1', [(DEV['colorStrip'], L9['color'], '#00ff00')], t0)
 

	
 
        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)
 
        self.assertEqual([[0, 0, 0, 0], 'flush',
 
                          [0, 0, 0, 0], 'flush',
 
                          [215, 0, 255, 0], 'flush'],
 
                         self.udmx.updates)
 
        self.assertEqual([[127, 0, 0, 0], 'flush',
 
                          [255, 0, 0, 0], 'flush',
 
                          [0, 0, 0, 0], 'flush'],
 
                         self.dmx0.updates)
 

	
 
    def testRepeatedAttributesInOneRequestGetResolved(self):
 
        c = Collector(self.config, outputs=[self.dmx0, self.udmx])
 
        
 
        c.setAttrs('client1', 'sess1', [
 
            (DEV['inst1'], L9['brightness'], .5),
 
            (DEV['inst1'], L9['brightness'], .3),
 
        ])
 
        self.assertEqual([[127], 'flush'], self.dmx0.updates)
 
        ], t0)
 
        self.assertEqual([[127, 0, 0, 0], 'flush'], self.dmx0.updates)
 

	
 
        c.setAttrs('client1', 'sess1', [
 
            (DEV['inst1'], L9['brightness'], .3),
 
            (DEV['inst1'], L9['brightness'], .5),
 
        ])
 
        self.assertEqual([[127], 'flush', [127], 'flush'], self.dmx0.updates)
 
        ], t0)
 
        self.assertEqual([[127, 0, 0, 0], 'flush',
 
                          [127, 0, 0, 0], 'flush'], self.dmx0.updates)
 

	
light9/collector/device.py
Show inline comments
 
@@ -55,49 +55,49 @@ def resolve(deviceType, deviceAttr, valu
 
        for v in values:
 
            if isinstance(v, Literal):
 
                floatVals.append(float(v.toPython()))
 
            elif isinstance(v, (int, float)):
 
                floatVals.append(float(v))
 
            else:
 
                raise TypeError(repr(v))
 
            
 
        return Literal(sum(floatVals) / len(floatVals))
 
    
 
    return max(values)
 

	
 
def toOutputAttrs(deviceType, deviceAttrSettings):
 
    """
 
    Given device attr settings like {L9['color']: Literal('#ff0000')},
 
    return a similar dict where the keys are output attrs (like
 
    L9['red']) and the values are suitable for Collector.setAttr
 

	
 
    :outputAttrRange happens before we get here.
 
    """
 
    def floatAttr(attr, default=0):
 
        out = deviceAttrSettings.get(attr)
 
        if out is None:
 
            return default
 
        return float(out.toPython())
 
        return float(out.toPython()) if isinstance(out, Literal) else out
 

	
 
    def rgbAttr(attr):
 
        color = deviceAttrSettings.get(attr, '#000000')
 
        r, g, b = hex_to_rgb(color)
 
        return r, g, b
 

	
 
    def cmyAttr(attr):
 
        rgb = sRGBColor.new_from_rgb_hex(deviceAttrSettings.get(attr, '#000000'))
 
        out = colormath.color_conversions.convert_color(rgb, CMYColor)
 
        return (
 
            _8bit(out.cmy_c),
 
            _8bit(out.cmy_m),
 
            _8bit(out.cmy_y))
 

	
 
    def fine16Attr(attr):
 
        x = floatAttr(attr)
 
        hi = _8bit(x)
 
        lo = _8bit((x * 255) % 1.0)
 
        return hi, lo
 
        
 
    if deviceType == L9['ChauvetColorStrip']:
 
        r, g, b = rgbAttr(L9['color'])
 
        return {
 
            L9['mode']: 215,
light9/collector/device_test.py
Show inline comments
 
import unittest
 
from rdflib import Literal
 
from light9.namespaces import L9
 

	
 
from light9.collector.device import toOutputAttrs, resolve
 

	
 
class TestUnknownDevice(unittest.TestCase):
 
    def testFails(self):
 
        self.assertRaises(NotImplementedError, toOutputAttrs, L9['bogus'], {})
 

	
 
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}))
 
        self.assertEqual({L9['level']: 127},
 
                         toOutputAttrs(L9['SimpleDimmer'], {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(127, out[L9['xFine']])
 
        self.assertEqual(47, out[L9['yRotation']])
 
        self.assertEqual(51, out[L9['yFine']])
 
        self.assertEqual(0, out[L9['rotationSpeed']])
 
        
 
class TestResolve(unittest.TestCase):
 
    def testMaxes1Color(self):
 
        # do not delete - this one catches a bug in the rgb_to_hex(...) lines
 
        self.assertEqual('#ff0300',
 
                         resolve(None, L9['color'], ['#ff0300']))
 
    def testMaxes2Colors(self):
 
        self.assertEqual('#ff0400',
 
                         resolve(None, L9['color'], ['#ff0300', '#000400']))
light9/collector/output.py
Show inline comments
 
@@ -2,48 +2,50 @@ from __future__ import division
 
from rdflib import URIRef
 
import sys
 
import time
 
import usb.core
 
import logging
 
from twisted.internet import task, threads, reactor
 
from greplin import scales
 
log = logging.getLogger('output')
 

	
 
# eliminate this: lists are always padded now
 
def setListElem(outList, index, value, fill=0, combine=lambda old, new: new):
 
    if len(outList) < index:
 
        outList.extend([fill] * (index - len(outList)))
 
    if len(outList) <= index:
 
        outList.append(value)
 
    else:
 
        outList[index] = combine(outList[index], value)
 

	
 
class Output(object):
 
    """
 
    send an array of values to some output device. Call update as
 
    often as you want- the result will be sent as soon as possible,
 
    and with repeats as needed to outlast hardware timeouts.
 
    """
 
    uri = None  # type: URIRef
 
    numChannels = None  # type: int
 
    def __init__(self):
 
        raise NotImplementedError
 
        
 
    def allConnections(self):
 
        """
 
        sequence of (index, uri) for the uris we can output, and which
 
        index in 'values' to use for them
 
        """
 
        raise NotImplementedError
 

	
 
    
 
    def update(self, values):
 
        """
 
        output takes a flattened list of values, maybe dmx channels, or
 
        pin numbers, etc
 
        """
 
        raise NotImplementedError
 

	
 
    def flush(self):
 
        """
 
        send latest data to output
 
        """
 
        raise NotImplementedError
 

	
light9/collector/output_test.py
Show inline comments
 
@@ -12,36 +12,28 @@ class TestSetListElem(unittest.TestCase)
 
        setListElem(x, 2, 9)
 
        self.assertEqual([0, 1, 9], x)
 
    def testSetBeyond(self):
 
        x = [0, 1]
 
        setListElem(x, 3, 9)
 
        self.assertEqual([0, 1, 0, 9], x)
 
    def testArbitraryFill(self):
 
        x = [0, 1]
 
        setListElem(x, 5, 9, fill=8)
 
        self.assertEqual([0, 1, 8, 8, 8, 9], x)
 
    def testSetZero(self):
 
        x = [0, 1]
 
        setListElem(x, 5, 0)
 
        self.assertEqual([0, 1, 0, 0, 0, 0], x)
 
    def testCombineMax(self):
 
        x = [0, 1]
 
        setListElem(x, 1, 0, combine=max)
 
        self.assertEqual([0, 1], x)
 
    def testCombineHasNoEffectOnNewElems(self):
 
        x = [0, 1]
 
        setListElem(x, 2, 1, combine=max)
 
        self.assertEqual([0, 1, 1], x)
 
        
 
class TestDmxOutput(unittest.TestCase):
 
    def testGeneratesConnectionList(self):
 
        out = DmxOutput(L9['output/udmx/'], 3)
 
        self.assertEqual([
 
            (0, L9['output/udmx/c1']),
 
            (1, L9['output/udmx/c2']),
 
            (2, L9['output/udmx/c3']),
 
        ], list(out.allConnections()))
 

	
 
    def testFlushIsNoop(self):
 
        out = DmxOutput(L9['output/udmx/'], 3)
 
        out.flush()
 
        
light9/io/__init__.py
Show inline comments
 
from __future__ import division
 
import sys
 

	
 
class BaseIO:
 
class BaseIO(object):
 
    def __init__(self):
 
        self.dummy=1
 
        self.__name__ = 'BaseIO'
 
        # please override and set __name__ to your class name
 

	
 
    def golive(self):
 
        """call this if you want to promote the dummy object becomes a live object"""
 
        print "IO: %s is going live" % self.__name__
 
        self.dummy=0
 
        # you'd override with additional startup stuff here,
 
        # perhaps even loading a module and saving it to a class
 
        # attr so the subclass-specific functions can use it
 

	
 
    def godummy(self):
 
        print "IO: %s is going dummy" % self.__name__
 
        self.dummy=1
 
        # you might override this to close ports, etc
 
        
 
    def isdummy(self):
 
        return self.dummy
 

	
 
    def __repr__(self):
 
        if self.dummy:
 
            return "<dummy %s instance>" % self.__name__
 
@@ -62,78 +62,24 @@ class UsbDMX(BaseIO):
 
        self.out = None
 
        self.dimmers = dimmers
 

	
 
    def _dmx(self):
 
        if self.out is None:
 
            if self.port == 'udmx':
 
                from udmx import Udmx
 
                self.out = Udmx()
 
                self.out.write = self.out.SendDMX
 
            else:
 
                sys.path.append("dmx_usb_module")
 
                from dmx import Dmx
 
                self.out = Dmx(self.port)
 
        return self.out
 

	
 
    def sendlevels(self, levels):
 
        if self.dummy:
 
            return
 
        # I was outputting on 76 and it was turning on the light at
 
        # dmx75. So I added the 0 byte.
 
        packet = '\x00' + ''.join([chr(int(lev * 255 / 100)) 
 
                                  for lev in levels]) + "\x55"
 
        self._dmx().write(packet)
 
        
 
class SerialPots(BaseIO):
 
    """
 
    this is a dummy object (that returns zeros forever) until you call startup()
 
    which makes it bind to the port, etc
 
    
 
    """
 
    def __init__(self):
 
        # no init here- call getport() to actually initialize
 
        self.dummy=1
 
        self.__name__='SerialPots' # i thought this was automatic!
 

	
 
    def golive(self):
 
        """
 
        ls -l /dev/i2c-0
 
        crw-rw-rw-    1 root     root      89,   0 Jul 11 12:27 /dev/i2c-0
 
        """
 
        import serport
 
        self.serport = serport
 
        
 
        self.f = open("/dev/i2c-0","rw")
 

	
 
        # this is for a chip with A0,A1,A2 lines all low:
 
        port = 72
 

	
 
        from fcntl import *
 

	
 
        I2C_SLAVE = 0x0703  #/* Change slave address                 */
 
        ioctl(self.f,I2C_SLAVE,port)
 
        self.dummy=0
 

	
 
    def godummy(self):
 
        BaseIO.godummy(self)
 
        self.f.close()
 

	
 
    def getlevels(self):
 
        if self.dummy:
 
            return (0,0,0,0)
 
        else:
 
            return self.serport.read_all_adc(self.f.fileno())
 

	
 

	
 
if __name__=='__main__':
 
    
 
    """ tester program that just dumps levels for a while """
 
    from time import sleep
 
    from serport import *
 

	
 
    i=0
 
    while i<100:
 
        sleep(.033)
 
        i=i+1
 

	
 
        print read_all_adc(f.fileno())
 

	
light9/rdfdb/graphfile_test.py
Show inline comments
 
@@ -6,34 +6,34 @@ from light9.rdfdb.graphfile import Graph
 

	
 
class TestGraphFileOutput(unittest.TestCase):
 
    def testMaintainsN3PrefixesFromInput(self):
 
        tf = tempfile.NamedTemporaryFile(suffix='_test.n3')
 
        tf.write('''
 
        @prefix : <http://example.com/> .
 
        @prefix n: <http://example.com/n/> .
 
        :foo n:bar :baz .
 
        ''')
 
        tf.flush()
 

	
 
        def getSubgraph(uri):
 
            return Graph()
 
        gf = GraphFile(mock.Mock(), tf.name, URIRef('uri'), mock.Mock(), getSubgraph)
 
        gf.reread()
 
        
 
        newGraph = Graph()
 
        newGraph.add((URIRef('http://example.com/boo'),
 
                      URIRef('http://example.com/n/two'),
 
                      URIRef('http://example.com/other/ns')))
 
        gf.dirty(newGraph)
 
        gf.flush()
 
        wroteContent = open(tf.name).read()
 
        self.assertEqual('''@prefix : <http://example.com/> .
 
@prefix dev: <http://light9.bigasterisk.com/device/> .
 
@prefix effect: <http://light9.bigasterisk.com/effect/> .
 
@prefix n: <http://example.com/n/> .
 
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
 
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
 
@prefix xml: <http://www.w3.org/XML/1998/namespace> .
 
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
 

	
 
:boo n:two <http://example.com/other/ns> .
 

	
 
''', wroteContent)
 

	
makefile
Show inline comments
 
@@ -81,24 +81,32 @@ bower:
 

	
 
raspberry_pi_virtualenv:
 
	mkdir -p env_pi
 
	virtualenv --system-site-packages env_pi
 

	
 
raspberry_pi_packages:
 
	sudo apt-get install python-picamera python-dev python-twisted python-virtualenv
 
	env_pi/bin/pip install cyclone 'coloredlogs==1.0.1'
 

	
 
darcs_show_checkpoint:
 
	darcs add --quiet --recursive ${LIGHT9_SHOW} 
 
	darcs rec -a -m "checkpoint show data" ${LIGHT9_SHOW}
 

	
 
/usr/share/arduino/Arduino.mk:
 
	sudo aptitude install arduino-mk
 

	
 
arduino_upload: /usr/share/arduino/Arduino.mk
 
	cd rgbled
 
	make upload
 

	
 
effect_node_setup: create_virtualenv packages binexec install_python_deps
 

	
 
coffee:
 
	zsh -c 'coffee -cw light9/web/{.,live,timeline}/*.coffee'
 

	
 
env-mypy/bin/mypy:
 
	mkdir -p env-mypy
 
	virtualenv -p /usr/bin/python3  env-mypy/
 
	env-mypy/bin/pip install mypy==0.501 lxml==3.7.3
 

	
 
mypy-collector: env-mypy/bin/mypy
 
	env-mypy/bin/mypy --py2 --ignore-missing-imports --strict-optional --custom-typeshed-dir stubs --html-report /tmp/rep bin/collector light9/collector/*.py
requirements.txt
Show inline comments
 
@@ -13,24 +13,26 @@ watchdog==0.8.3
 
freezegun==0.3.7
 
ipdb==0.8.1
 
coloredlogs==1.0.1
 
genshi==0.7
 
pyjade==3.0.0
 
python-dateutil==2.4.2
 
txosc==0.2.0
 
service_identity==0.2
 
Pillow==2.8.1
 
faulthandler==2.3
 
treq==0.2.1
 
mock==1.0.1
 
toposort==1.0
 
pyserial==2.7
 

	
 
statprof==0.1.2
 
txzmq==0.7.4
 

	
 
pyusb==1.0.0
 
coverage==4.1
 
klein==15.3.1
 
git+http://github.com/drewp/scales.git@448d59fb491b7631877528e7695a93553bfaaa93#egg=scales
 
colormath==2.1.1
 
noise==1.2.2
 

	
 
typing==3.6.1
0 comments (0 inline, 0 general)