# HG changeset patch # User Drew Perttula # Date 2016-05-30 11:13:06 # Node ID d51014267bfda0742f7ca19162dd422e85914ddb # Parent 02415a3cebb292e2257eaa4862aef24f1fca047f move device-specific code out of collector. resolver isn't done yet. live.html can edit colors Ignore-this: d537a0418ffde35b928387706977cbe0 diff --git a/light9/collector/collector.py b/light9/collector/collector.py --- a/light9/collector/collector.py +++ b/light9/collector/collector.py @@ -1,16 +1,12 @@ 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) @@ -55,12 +51,19 @@ class Collector(object): 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. """ @@ -77,28 +80,29 @@ class Collector(object): 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""" diff --git a/light9/collector/collector_test.py b/light9/collector/collector_test.py --- a/light9/collector/collector_test.py +++ b/light9/collector/collector_test.py @@ -28,7 +28,7 @@ class MockOutput(object): def allConnections(self): return self.connections - + def update(self, values): self.updates.append(values) @@ -63,26 +63,26 @@ class TestOutputMap(unittest.TestCase): 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 . ''') @@ -92,68 +92,62 @@ class TestCollector(unittest.TestCase): (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): @@ -161,10 +155,10 @@ class TestCollector(unittest.TestCase): 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]) @@ -177,11 +171,12 @@ class TestCollector(unittest.TestCase): 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) diff --git a/light9/collector/device.py b/light9/collector/device.py new file mode 100644 --- /dev/null +++ b/light9/collector/device.py @@ -0,0 +1,82 @@ +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) diff --git a/light9/collector/device_test.py b/light9/collector/device_test.py new file mode 100644 --- /dev/null +++ b/light9/collector/device_test.py @@ -0,0 +1,35 @@ +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']]) diff --git a/light9/web/lib/bower.json b/light9/web/lib/bower.json --- a/light9/web/lib/bower.json +++ b/light9/web/lib/bower.json @@ -9,5 +9,9 @@ "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" } } diff --git a/light9/web/live.html b/light9/web/live.html --- a/light9/web/live.html +++ b/light9/web/live.html @@ -14,9 +14,21 @@ - - +
+

moving

+ rx + ry + color + + diff --git a/makefile b/makefile --- a/makefile +++ b/makefile @@ -1,4 +1,4 @@ -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)