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 @@