Changeset - ffae830fda12
[Not reviewed]
light9/effect/effecteval.py
Show inline comments
 
from dataclasses import dataclass
 
import logging
 
import math
 
import random
 
from colorsys import hsv_to_rgb
 
from typing import Any, Dict, Tuple
 
from light9.effect.simple_outputs import SimpleOutputs
 
from light9.newtypes import DeviceAttr, DeviceUri, EffectAttr, EffectClass, VTUnion
 
from dataclasses import dataclass
 
from typing import Dict, Tuple
 

	
 
from noise import pnoise1
 
from PIL import Image
 
from rdflib import Literal, Namespace, URIRef
 
from rdfdb.syncedgraph.syncedgraph import SyncedGraph
 
from rdflib import Literal, Namespace
 
from webcolors import hex_to_rgb, rgb_to_hex
 

	
 
from light9.effect.scale import scale
 
from light9.effect.settings import BareEffectSettings, DeviceSettings, EffectSettings
 
from light9.effect.settings import BareEffectSettings, EffectSettings
 
from light9.effect.simple_outputs import SimpleOutputs
 
from light9.namespaces import DEV, L9
 
from rdfdb.syncedgraph.syncedgraph import SyncedGraph
 
from light9.newtypes import (DeviceAttr, DeviceUri, EffectAttr, EffectClass, VTUnion)
 

	
 
SKY = Namespace('http://light9.bigasterisk.com/theater/skyline/device/')
 

	
 
random.seed(0)
 

	
 
log = logging.getLogger('effecteval')
 
log.info("reload effecteval")
 

	
 

	
 
def literalColor(rnorm, gnorm, bnorm):
 
    return Literal(rgb_to_hex((
 
        int(rnorm * 255),  #
 
        int(gnorm * 255),  # 
 
        int(gnorm * 255),  #
 
        int(bnorm * 255))))
 

	
 

	
 
def literalColorHsv(h, s, v):
 
    return literalColor(*hsv_to_rgb(h, s, v))
 

	
light9/effect/scale.py
Show inline comments
 
from decimal import Decimal
 

	
 
from rdflib import Literal
 
from decimal import Decimal
 
from webcolors import rgb_to_hex, hex_to_rgb
 
from webcolors import hex_to_rgb, rgb_to_hex
 

	
 

	
 
def scale(value, strength):
 
    if isinstance(value, Literal):
 
        value = value.toPython()
 

	
 
@@ -18,11 +19,11 @@ def scale(value, strength):
 
            if isinstance(strength, Literal):
 
                strength = strength.toPython()
 
            if isinstance(strength, str):
 
                sr, sg, sb = [v / 255 for v in hex_to_rgb(strength)]
 
            else:
 
                sr = sg = sb = strength
 
            return rgb_to_hex([int(r * sr), int(g * sg), int(b * sb)])
 
            return rgb_to_hex((int(r * sr), int(g * sg), int(b * sb)))
 
    elif isinstance(value, (int, float)):
 
        return value * strength
 

	
 
    raise NotImplementedError("%r,%r" % (value, strength))
light9/effect/sequencer/eval_faders.py
Show inline comments
 
@@ -15,13 +15,13 @@ from light9.namespaces import L9, RDF
 
from light9.newtypes import EffectAttr, NoteUri, UnixTime
 

	
 
log = logging.getLogger('seq.fader')
 

	
 
class FaderEval:
 
    """peer to Sequencer, but this one takes the current :Fader settings -> sendToCollector
 
    
 

	
 
    The current faders become Notes in here, for more code reuse.
 
    """
 
    def __init__(self,
 
                 graph: SyncedGraph,
 
                 sendToCollector: Callable[[DeviceSettings], Coroutine[None ,None,None]],
 
                 ):
 
@@ -50,38 +50,38 @@ class FaderEval:
 
        #self.updateLoop()
 

	
 
    @metrics('compile_graph_fader').time()
 
    def compileGraph(self) -> None:
 
        """rebuild our data from the graph"""
 
        self.notes = []
 
        for fader in self.graph.subjects(RDF.type, L9['Fader']):          
 
        for fader in self.graph.subjects(RDF.type, L9['Fader']):
 
            def compileFader() -> Note:
 
                return self.compileFader(cast(URIRef, fader))
 

	
 
            self.notes.append(compileFader())
 
        # if self.notes:
 
        #     asyncio.create_task(self.startUpdating())
 

	
 

	
 
    @metrics('compile_fader').time()
 
    def compileFader(self, fader: URIRef) -> Note:
 
        return Note(self.graph, cast(NoteUri, fader), 
 
        return Note(self.graph, cast(NoteUri, fader),
 
                    timed=False)
 
    
 

	
 
    def computeOutput(self) -> DeviceSettings:
 
        notesSettings = []
 
        now = UnixTime(time.time())
 
        for note in self.notes:
 
            effectSettings, report = note.outputCurrent()
 

	
 
            if effectSettings.s[EffectAttr(L9['strength'])]==0:
 
                continue
 

	
 
            ee = effecteval.EffectEval(self.graph, note.effectClass, self.simpleOutputs)
 
            deviceSettings, report = ee.outputFromEffect(
 
                effectSettings, 
 
                effectSettings,
 
                songTime=now, # probably wrong
 
                noteTime=now, # wrong
 
                )
 
            if deviceSettings:
 
                notesSettings.append(deviceSettings)
 
        return DeviceSettings.merge(self.graph, notesSettings)
light9/effect/sequencer/eval_faders_test.py
Show inline comments
 
from unittest import mock
 

	
 
from light9.effect.sequencer.eval_faders import FaderEval
 
from light9.effect.settings import DeviceSettings
 
from light9.mock_syncedgraph import MockSyncedGraph
 
from light9.namespaces import L9
 

	
 

	
 
PREFIXES = '''
 
@prefix : <http://light9.bigasterisk.com/> .
 
@prefix effect: <http://light9.bigasterisk.com/effect/> .
 
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
 
@prefix show: <http://light9.bigasterisk.com/show/dance2023/> .
 
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
 
@prefix dev: <http://light9.bigasterisk.com/theater/test/device/> .
 
@prefix dmxA: <http://light9.bigasterisk.com/output/dmxA/> .
 
'''
 

	
 
NOTE_GRAPH = PREFIXES + '''
 
            :brightness         
 
                a :DeviceAttr; 
 
                rdfs:label "brightness"; 
 
            :brightness
 
                a :DeviceAttr;
 
                rdfs:label "brightness";
 
                :dataType :scalar .
 

	
 
            :strength
 
                a :EffectAttr;
 
                rdfs:label "strength" .
 

	
 
            :SimpleDimmer 
 
                a :DeviceClass; 
 
            :SimpleDimmer
 
                a :DeviceClass;
 
                rdfs:label "SimpleDimmer";
 
                :deviceAttr :brightness;
 
                :attr [ :outputAttr :level; :dmxOffset 0 ] .
 
                
 
            :light1  
 

	
 
            :light1
 
                a :SimpleDimmer;
 
                :dmxUniverse dmxA:;
 
                :dmxBase 178 .
 

	
 
            effect:effect1 
 
                a :EffectClass;
 
            effect:effect1
 
                a :Effect;
 
                :setting effect:effect1_set1 .
 
            effect:effect1_set1 
 
                :device :light1; 
 
                :deviceAttr :brightness; 
 
            effect:effect1_set1
 
                :device :light1;
 
                :deviceAttr :brightness;
 
                :scaledValue 0.5 .
 
            :fade1 
 
                a :Fader; 
 
                :effectClass effect:effect1; 
 
                :effectAttr :strength; 
 
            :fade1
 
                a :Fader;
 
                :effectClass effect:effect1;
 
                :effectAttr :strength;
 
                :value 0.6 .
 

	
 
        '''
 

	
 

	
 
class TestFaderEval:
 

	
 
    def test_faderValueScalesEffectSettings(self):
 
        g = MockSyncedGraph(NOTE_GRAPH)
 
        sender = mock.MagicMock()
 
        
 

	
 
        f = FaderEval(g, sender)
 
        devSettings = f.computeOutput()
 
        assert devSettings == DeviceSettings(g, [(L9['light1'], L9['brightness'], 0.3)])
 
\ No newline at end of file
light9/effect/sequencer/note.py
Show inline comments
 
import bisect
 
from dataclasses import dataclass
 
import logging
 
import time
 
from dataclasses import dataclass
 
from decimal import Decimal
 
from typing import Any, Dict, List, Optional, Tuple, Union, cast
 
from light9.effect.effecteval import EffectEval
 
from light9.effect.settings import BareEffectSettings, DeviceSettings, EffectSettings
 
from light9.effect.simple_outputs import SimpleOutputs
 

	
 
from rdfdb.syncedgraph.syncedgraph import SyncedGraph
 
from rdflib import Literal, URIRef
 
from rdflib import Literal
 

	
 
from light9.effect.settings import BareEffectSettings
 
from light9.namespaces import L9
 
from light9.newtypes import Curve, DeviceAttr, EffectAttr, EffectClass, NoteUri, VTUnion, typedValue
 
from light9.newtypes import (Curve, EffectAttr, EffectClass, NoteUri, VTUnion, typedValue)
 

	
 
log = logging.getLogger('sequencer')
 

	
 

	
 
def pyType(n):
 
    ret = n.toPython()
 
@@ -30,34 +28,34 @@ def prettyFormat(x: Union[float, str]):
 
    return x
 

	
 

	
 
@dataclass
 
class Note:
 
    """A Note outputs EffectAttr settings.
 
    
 

	
 
    Sample graph:
 
     :note1 a :Note; :curve :n1c1; :effectClass effect:allcolor;
 

	
 
    It can animate the EffectAttr settings over time, in two ways:
 
     * a `timed note` has an envelope curve that animates 
 
     * a `timed note` has an envelope curve that animates
 
       the :strength EffectAttr over time
 
     * an `untimed note` has no curve, a fixed strength, but 
 
       still passes the wall clock time to its effect so the 
 
     * an `untimed note` has no curve, a fixed strength, but
 
       still passes the wall clock time to its effect so the
 
       effect can include animation. A `Fader` is an untimed note.
 
     
 
    This obj is immutable, I think, but the graph can change, 
 
    which can affect the output. However, I think this doesn't 
 
    do its own rebuilds, and it's up to the caller to addHandler 
 

	
 
    This obj is immutable, I think, but the graph can change,
 
    which can affect the output. However, I think this doesn't
 
    do its own rebuilds, and it's up to the caller to addHandler
 
    around the creation of Note objects.
 
    """
 
    graph: SyncedGraph
 
    uri: NoteUri
 
    # simpleOutputs: SimpleOutputs
 
    timed: bool = True
 

	
 
    def __post_init__(self): # graph ok
 
    def __post_init__(self):  # graph ok
 
        ec = self.graph.value(self.uri, L9['effectClass'])
 
        if ec is None:
 
            raise ValueError(f'note {self.uri} has no :effectClass')
 
        self.effectClass = EffectClass(ec)
 

	
 
        self.baseEffectSettings = self.getBaseEffectSettings()
 
@@ -67,16 +65,16 @@ class Note:
 
            self.points: List[Tuple[float, float]] = []
 
            for curve in self.graph.objects(self.uri, L9['curve']):
 
                self.points.extend(self.getCurvePoints(cast(Curve, curve), L9['strength'], originTime))
 
            self.points.sort()
 
        else:
 
            self.points = []
 
            self.value =  typedValue(float, self.graph, self.uri, L9['value'])
 
            self.value = typedValue(float, self.graph, self.uri, L9['value'])
 

	
 
    def getBaseEffectSettings(self) -> BareEffectSettings: # graph ok
 
        """i think these are settings that are fixed over time, 
 
    def getBaseEffectSettings(self) -> BareEffectSettings:  # graph ok
 
        """i think these are settings that are fixed over time,
 
        e.g. that you set in the note's body in the timeline editor
 
        """
 
        out: Dict[EffectAttr, VTUnion] = {}
 
        for s in self.graph.objects(self.uri, L9['setting']):
 
            settingValues = dict(self.graph.predicate_objects(s))
 
            ea = cast(EffectAttr, settingValues[L9['effectAttr']])
 
@@ -116,20 +114,20 @@ class Note:
 
        p1, p2 = self.points[i], self.points[i + 1]
 
        frac = (t - p1[0]) / (p2[0] - p1[0])
 
        y = p1[1] + (p2[1] - p1[1]) * frac
 
        return y
 

	
 
    def outputCurrent(self):  # no graph
 
        
 

	
 
        return self._outputSettings(t=None, strength=self.value)
 

	
 
    def _outputSettings(
 
            self,
 
            t: float | None,
 
            strength: Optional[float] = None  #
 
    ) -> Tuple[BareEffectSettings, Dict]: # no graph
 
    ) -> Tuple[BareEffectSettings, Dict]:  # no graph
 

	
 
        if t is None:
 
            if self.timed:
 
                raise TypeError()
 
            t = time.time()  # so live effects will move
 
        report: Dict[str, Any] = {
light9/effect/sequencer/note_test.py
Show inline comments
 
@@ -14,16 +14,16 @@ PREFIXES = '''
 
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
 
@prefix dev: <http://light9.bigasterisk.com/theater/test/device/> .
 
@prefix dmxA: <http://light9.bigasterisk.com/output/dmxA/> .
 
'''
 

	
 
FADER_GRAPH = PREFIXES + '''
 
    :fade1 
 
        a :Fader; 
 
        :effectClass effect:effect1; 
 
        :effectAttr :strength; 
 
    :fade1
 
        a :Fader;
 
        :effectClass effect:effect1;
 
        :effectAttr :strength;
 
        :value 0.6 .
 
'''
 

	
 

	
 
class TestUntimedFaderNote:
 

	
 
@@ -33,45 +33,44 @@ class TestUntimedFaderNote:
 
        out, report = n.outputCurrent()
 
        assert report['effectSettings'] == {'http://light9.bigasterisk.com/strength': 0.6}
 
        assert out == BareEffectSettings(s={EffectAttr(L9['strength']): 0.6})
 

	
 

	
 
NOTE_GRAPH = PREFIXES + '''
 
            :brightness         
 
                a :DeviceAttr; 
 
                rdfs:label "brightness"; 
 
                :dataType :scalar .
 
    :brightness
 
        a :DeviceAttr;
 
        rdfs:label "brightness";
 
        :dataType :scalar .
 

	
 
            :strength
 
                a :EffectAttr;
 
                rdfs:label "strength" .
 
    :strength
 
        a :EffectAttr;
 
        rdfs:label "strength" .
 

	
 
    :SimpleDimmer
 
        a :DeviceClass;
 
        rdfs:label "SimpleDimmer";
 
        :deviceAttr :brightness;
 
        :attr [ :outputAttr :level; :dmxOffset 0 ] .
 

	
 
            :SimpleDimmer 
 
                a :DeviceClass; 
 
                rdfs:label "SimpleDimmer";
 
                :deviceAttr :brightness;
 
                :attr [ :outputAttr :level; :dmxOffset 0 ] .
 
                
 
            dev:light1  
 
                a :SimpleDimmer;
 
                :dmxUniverse dmxA:;
 
                :dmxBase 178 .
 
    dev:light1
 
        a :SimpleDimmer;
 
        :dmxUniverse dmxA:;
 
        :dmxBase 178 .
 

	
 
            effect:effect1 
 
                a :EffectClass;
 
                :setting effect:effect1_set1 .
 
            effect:effect1_set1 
 
                :device dev:light1; 
 
                :deviceAttr :brightness; 
 
                :scaledValue 0.5 .
 
            :fade1 
 
                a :Fader; 
 
                :effectClass effect:effect1; 
 
                :effectAttr :strength; 
 
                :value 0.6 .
 

	
 
    effect:effect1
 
        a :EffectClass;
 
        :setting effect:effect1_set1 .
 
    effect:effect1_set1
 
        :device dev:light1;
 
        :deviceAttr :brightness;
 
        :scaledValue 0.5 .
 
    :fade1
 
        a :Fader;
 
        :effectClass effect:effect1;
 
        :effectAttr :strength;
 
        :value 0.6 .
 
        '''
 

	
 

	
 
class TestTimedNote:
 

	
 
    @pytest.mark.skip()
light9/effect/sequencer/service.py
Show inline comments
 
@@ -42,29 +42,34 @@ async def changes():
 
            yield json.dumps(state)
 

	
 

	
 
async def send_page_updates(request):
 
    return EventSourceResponse(changes())
 

	
 

	
 
###################################################################
 

	
 

	
 
async def _send_one(faders:FaderEval):
 
async def _send_one(faders: FaderEval):
 
    ds = faders.computeOutput()
 
    await sendToCollector('effectSequencer', session='0', settings=ds)
 

	
 

	
 
async def _forever(faders):
 
    while True:
 
        await _send_one(faders)
 
        await asyncio.sleep(0.1)
 

	
 

	
 
def send_updates_forever(faders):
 
    asyncio.create_task(_forever(faders))
 

	
 

	
 
####################################################################
 

	
 

	
 
def main():
 
    session = 'effectSequencer'
 
    graph = SyncedGraph(networking.rdfdb.url, "effectSequencer")
 
    logging.getLogger('autodepgraphapi').setLevel(logging.INFO)
 
    logging.getLogger('syncedgraph').setLevel(logging.INFO)
 
    logging.getLogger('sse_starlette.sse').setLevel(logging.INFO)
light9/effect/sequencer/service_test.py
Show inline comments
 
import asyncio
 

	
 
import asyncio
 
from light9.run_local import log
 

	
 

	
 
def test_import():
 

	
 
    async def go():
light9/effect/settings.py
Show inline comments
 
@@ -7,22 +7,22 @@ BareSettings means (attr,value), no devi
 
"""
 
from __future__ import annotations
 

	
 
import decimal
 
import logging
 
from dataclasses import dataclass
 
from typing import Any, Dict, Iterable, List, Sequence, Set, Tuple, Union, cast
 
from light9.localsyncedgraph import LocalSyncedGraph
 
from typing import Any, Dict, Iterable, List, Sequence, Set, Tuple, cast
 

	
 
import numpy
 
from rdfdb.syncedgraph.syncedgraph import SyncedGraph
 
from rdflib import Literal, URIRef
 

	
 
from light9.collector.device import resolve
 
from light9.localsyncedgraph import LocalSyncedGraph
 
from light9.namespaces import L9, RDF
 
from light9.newtypes import DeviceAttr, DeviceUri, EffectAttr, HexColor, VTUnion
 
from rdfdb.syncedgraph.syncedgraph import SyncedGraph
 
from light9.newtypes import (DeviceAttr, DeviceUri, EffectAttr, HexColor, VTUnion)
 

	
 
log = logging.getLogger('settings')
 

	
 

	
 
def parseHex(h):
 
    if h[0] != '#':
light9/effect/settings_test.py
Show inline comments
 
import unittest
 
from typing import cast
 
import unittest
 
from light9.newtypes import DeviceAttr, DeviceUri, HexColor, VTUnion
 

	
 
from rdfdb.patch import Patch
 
from rdflib import Literal
 
from rdfdb.patch import Patch
 

	
 
from light9.effect.settings import DeviceSettings
 
from light9.localsyncedgraph import LocalSyncedGraph
 
from light9.namespaces import L9, DEV
 
from light9.effect.settings import DeviceSettings
 
from light9.namespaces import DEV, L9
 
from light9.newtypes import DeviceAttr, DeviceUri, HexColor, VTUnion
 

	
 

	
 
class TestDeviceSettings(unittest.TestCase):
 

	
 
    def setUp(self):
 
        self.graph = LocalSyncedGraph(
 
            files=['test/cam/lightConfig.n3', 'test/cam/bg.n3'])
 
        self.graph = LocalSyncedGraph(files=['test/cam/lightConfig.n3', 'test/cam/bg.n3'])
 

	
 
    def testToVectorZero(self):
 
        ds = DeviceSettings(self.graph, [])
 
        self.assertEqual([0] * 30, ds.toVector())
 

	
 
    def testEq(self):
 
@@ -28,20 +29,18 @@ class TestDeviceSettings(unittest.TestCa
 
            (L9['light1'], L9['attr1'], 0.5),
 
        ])
 
        self.assertTrue(s1 == s2)
 
        self.assertFalse(s1 != s2)
 

	
 
    def testMissingFieldsEqZero(self):
 
        self.assertEqual(
 
            DeviceSettings(self.graph, [
 
                (L9['aura1'], L9['rx'], 0),
 
            ]), DeviceSettings(self.graph, []))
 
        self.assertEqual(DeviceSettings(self.graph, [
 
            (L9['aura1'], L9['rx'], 0),
 
        ]), DeviceSettings(self.graph, []))
 

	
 
    def testFalseIfZero(self):
 
        self.assertTrue(
 
            DeviceSettings(self.graph, [(L9['aura1'], L9['rx'], 0.1)]))
 
        self.assertTrue(DeviceSettings(self.graph, [(L9['aura1'], L9['rx'], 0.1)]))
 
        self.assertFalse(DeviceSettings(self.graph, []))
 

	
 
    def testFromResource(self):
 
        ctx = L9['']
 
        self.graph.patch(
 
            Patch(addQuads=[
 
@@ -53,43 +52,36 @@ class TestDeviceSettings(unittest.TestCa
 
                (L9['foo_set1'], L9['device'], L9['light1'], ctx),
 
                (L9['foo_set1'], L9['deviceAttr'], L9['speed'], ctx),
 
                (L9['foo_set1'], L9['scaledValue'], Literal(0.2), ctx),
 
            ]))
 
        s = DeviceSettings.fromResource(self.graph, DeviceUri(L9['foo']))
 

	
 
        self.assertEqual(
 
            DeviceSettings(self.graph, [
 
                (L9['light1'], L9['brightness'], 0.1),
 
                (L9['light1'], L9['speed'], 0.2),
 
            ]), s)
 
        self.assertEqual(DeviceSettings(self.graph, [
 
            (L9['light1'], L9['brightness'], 0.1),
 
            (L9['light1'], L9['speed'], 0.2),
 
        ]), s)
 

	
 
    def testToVector(self):
 
        v = DeviceSettings(self.graph, [
 
            (DeviceUri(DEV['aura1']), DeviceAttr(L9['rx']), 0.5),
 
            (DeviceUri(DEV['aura1']), DeviceAttr(L9['color']), HexColor('#00ff00')),
 
        ]).toVector()
 
        self.assertEqual([
 
            0, 1, 0, 0.5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
 
            0, 0, 0, 0, 0, 0, 0, 0
 
        ], v)
 
        self.assertEqual([0, 1, 0, 0.5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], v)
 

	
 
    def testFromVector(self):
 
        s = DeviceSettings.fromVector(self.graph, [
 
            0, 1, 0, 0.5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
 
            0, 0, 0, 0, 0, 0, 0, 0
 
        ])
 
        s = DeviceSettings.fromVector(self.graph, [0, 1, 0, 0.5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
 

	
 
        self.assertEqual(
 
            DeviceSettings(self.graph, [
 
                (DeviceUri(DEV['aura1']), DeviceAttr(L9['rx']), 0.5),
 
                (DeviceUri(DEV['aura1']), DeviceAttr(L9['color']), HexColor('#00ff00')),
 
            ]), s)
 

	
 
    def testAsList(self):
 
        sets = [
 
            (DeviceUri(L9['light1']), DeviceAttr(L9['attr2']), cast(VTUnion,0.3)),
 
            (DeviceUri(L9['light1']), DeviceAttr(L9['attr2']), cast(VTUnion, 0.3)),
 
            (DeviceUri(L9['light1']), DeviceAttr(L9['attr1']), 0.5),
 
        ]
 
        self.assertCountEqual(sets, DeviceSettings(self.graph, sets).asList())
 

	
 
    def testDevices(self):
 
        s = DeviceSettings(self.graph, [
 
@@ -129,28 +121,21 @@ L1 = L9['light1']
 
ZOOM = L9['zoom']
 

	
 

	
 
class TestFromBlend(unittest.TestCase):
 

	
 
    def setUp(self):
 
        self.graph = LocalSyncedGraph(
 
            files=['test/cam/lightConfig.n3', 'test/cam/bg.n3'])
 
        self.graph = LocalSyncedGraph(files=['test/cam/lightConfig.n3', 'test/cam/bg.n3'])
 

	
 
    def testSingle(self):
 
        self.assertEqual(
 
            DeviceSettings(self.graph, [(L1, ZOOM, 0.5)]),
 
            DeviceSettings.fromBlend(
 
                self.graph,
 
                [(1, DeviceSettings(self.graph, [(L1, ZOOM, 0.5)]))]))
 
        self.assertEqual(DeviceSettings(self.graph, [(L1, ZOOM, 0.5)]),
 
                         DeviceSettings.fromBlend(self.graph, [(1, DeviceSettings(self.graph, [(L1, ZOOM, 0.5)]))]))
 

	
 
    def testScale(self):
 
        self.assertEqual(
 
            DeviceSettings(self.graph, [(L1, ZOOM, 0.1)]),
 
            DeviceSettings.fromBlend(
 
                self.graph,
 
                [(.2, DeviceSettings(self.graph, [(L1, ZOOM, 0.5)]))]))
 
        self.assertEqual(DeviceSettings(self.graph, [(L1, ZOOM, 0.1)]),
 
                         DeviceSettings.fromBlend(self.graph, [(.2, DeviceSettings(self.graph, [(L1, ZOOM, 0.5)]))]))
 

	
 
    def testMixFloats(self):
 
        self.assertEqual(
 
            DeviceSettings(self.graph, [(L1, ZOOM, 0.4)]),
 
            DeviceSettings.fromBlend(self.graph, [
 
                (.2, DeviceSettings(self.graph, [(L1, ZOOM, 0.5)])),
light9/effect/simple_outputs.py
Show inline comments
 
@@ -9,26 +9,26 @@ from light9.namespaces import L9, RDF
 

	
 
log = logging.getLogger('simple')
 

	
 

	
 
class SimpleOutputs:
 
    """
 
    Watches graph for effects that are just fading output attrs. 
 
    Watches graph for effects that are just fading output attrs.
 
    Call `values` to get (dev,attr):value settings.
 
    """
 

	
 
    def __init__(self, graph):
 
        self.graph = graph
 

	
 
        # effect : [(dev, attr, value, isScaled)]
 
        self.effectOutputs: Dict[URIRef, List[Tuple[URIRef, URIRef, Any, bool]]] = {}
 

	
 
        self.graph.addHandler(self.updateEffectsFromGraph)
 

	
 
    def updateEffectsFromGraph(self):
 
        self.effectOutputs={}
 
        self.effectOutputs = {}
 
        for effect in self.graph.subjects(RDF.type, L9['Effect']):
 
            log.debug(f' {effect=}')
 
            settings = []
 
            for setting in self.graph.objects(effect, L9['setting']):
 
                settingValues = dict(self.graph.predicate_objects(setting))
 
                try:
light9/newtypes.py
Show inline comments
 
import decimal
 
from typing import Tuple, NewType, Type, TypeVar, Union, cast
 
from typing import NewType, Tuple, Type, TypeVar, Union, cast
 

	
 
from rdflib import URIRef
 
from rdflib.term import Node
 

	
 
ClientType = NewType('ClientType', str)
 
ClientSessionType = NewType('ClientSessionType', str)
 
Curve = NewType('Curve', URIRef)
 
OutputUri = NewType('OutputUri', URIRef)  # e.g. dmxA
 
DeviceUri = NewType('DeviceUri', URIRef)  # e.g. :aura2
 
DeviceClass = NewType('DeviceClass', URIRef)  # e.g. :Aura
 
DmxIndex = NewType('DmxIndex', int)  # 1..512
 
DmxMessageIndex = NewType('DmxMessageIndex', int)  # 0..511
 
DeviceAttr = NewType('DeviceAttr', URIRef)  # e.g. :rx
 
EffectClass = NewType('EffectClass', URIRef) # e.g. effect:chase
 
EffectClass = NewType('EffectClass', URIRef)  # e.g. effect:chase
 
EffectAttr = NewType('EffectAttr', URIRef)  # e.g. :chaseSpeed
 
NoteUri = NewType('NoteUri', URIRef)
 
OutputAttr = NewType('OutputAttr', URIRef)  # e.g. :xFine
 
OutputValue = NewType('OutputValue', int)  # byte in dmx message
 
Song = NewType('Song', URIRef)
 
UnixTime = NewType('UnixTime', float)
 
@@ -39,13 +40,13 @@ def _isSubclass2(t1: Type, t2: Type) -> 
 
    if hasattr(t1, '__supertype__'):
 
        t1 = t1.__supertype__
 
    return issubclass(t1, t2)
 

	
 

	
 
def typedValue(objType: Type[_ObjType], graph, subj, pred) -> _ObjType:
 
    """graph.value(subj, pred) with a given return type. 
 
    """graph.value(subj, pred) with a given return type.
 
    If objType is not an rdflib.Node, we toPython() the value."""
 
    obj = graph.value(subj, pred)
 
    if obj is None:
 
        raise ValueError()
 
    conv = obj if _isSubclass2(objType, Node) else obj.toPython()
 
    if objType is float and isinstance(conv, decimal.Decimal):
0 comments (0 inline, 0 general)