Changeset - 091909b4b727
[Not reviewed]
default
0 3 1
drewp@bigasterisk.com - 20 months ago 2023-05-22 08:08:31
drewp@bigasterisk.com
seems kind of important that effecteval return DeviceSettings, not more EffectSettings
4 files changed with 48 insertions and 8 deletions:
0 comments (0 inline, 0 general)
light9/collector/collector.py
Show inline comments
 
@@ -136,51 +136,51 @@ class Collector:
 
        dt1 = time.time() - now
 

	
 
        self._updateOutputs(pendingOut)
 

	
 
        dt2 = time.time() - dt1
 
        if dt1 > .030 or dt2 > .030:
 
            log.warning("slow setAttrs: prepare %.1fms -> updateOutputs %.1fms" % (dt1 * 1000, dt2 * 1000))
 

	
 
    def _warnOnLateRequests(self, client, now, sendTime):
 
        requestLag = now - sendTime
 
        if requestLag > .1 and now > self._initTime + 10 and getattr(self, '_lastWarnTime', 0) < now - 3:
 
            self._lastWarnTime = now
 
            log.warning('collector.setAttrs from %s is running %.1fms after the request was made', client, requestLag * 1000)
 

	
 
    def _forgetStaleClients(self, now):
 
        staleClientSessions = []
 
        for clientSession, (reqTime, _) in self.lastRequest.items():
 
            if reqTime < now - self.clientTimeoutSec:
 
                staleClientSessions.append(clientSession)
 
        for clientSession in staleClientSessions:
 
            log.info('forgetting stale client %r', clientSession)
 
            del self.lastRequest[clientSession]
 

	
 
    # todo: move to settings.py
 
    def resolvedSettingsDict(self, settingsList: List[DeviceSetting]) -> Dict[Tuple[DeviceUri, DeviceAttr], VTUnion]:
 
    def _resolvedSettingsDict(self, settingsList: DeviceSettings) -> Dict[Tuple[DeviceUri, DeviceAttr], VTUnion]:
 
        out: Dict[Tuple[DeviceUri, DeviceAttr], VTUnion] = {}
 
        for devUri, devAttr, val in settingsList:
 
        for devUri, devAttr, val in settingsList.asList():
 
            if (devUri, devAttr) in out:
 
                existingVal = out[(devUri, devAttr)]
 
                out[(devUri, devAttr)] = resolve(self._deviceType[devUri], devAttr, [existingVal, val])
 
            else:
 
                out[(devUri, devAttr)] = val
 
        return out
 

	
 
    def _merge(self, lastRequests):
 
        deviceAttrs: Dict[DeviceUri, Dict[DeviceAttr, VTUnion]] = {}  # device: {deviceAttr: value}
 
        for _, lastSettings in lastRequests:
 
            for (device, deviceAttr), value in lastSettings.items():
 
                if (device, deviceAttr) in self.remapOut:
 
                    start, end = self.remapOut[(device, deviceAttr)]
 
                    value = 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)] = cast(float, value)
light9/collector/service.py
Show inline comments
 
@@ -36,49 +36,49 @@ class Updates(WebSocketEndpoint, UiListe
 
    def __init__(self, listeners, scope: Scope, receive: Receive, send: Send) -> None:
 
        super().__init__(scope, receive, send)
 
        self.listeners = listeners
 

	
 
    async def on_connect(self, websocket: WebSocket):
 
        await websocket.accept()
 
        log.info('socket connect %s', self.scope['client'])
 
        self.websocket = websocket
 
        self.listeners.addClient(self)
 

	
 
    async def sendMessage(self, msgText):
 
        await self.websocket.send_text(msgText)
 

	
 
    # async def on_receive(self, websocket, data):
 
    #     json.loads(data)
 

	
 
    async def on_disconnect(self, websocket: WebSocket, close_code: int):
 
        self.listeners.delClient(self)
 

	
 
    pass
 

	
 

	
 
async def PutAttrs(collector: Collector, request):
 
    with STAT_SETATTR.time():
 
        client, clientSession, settings, sendTime = parseJsonMessage(await request.body())
 
        client, clientSession, settings, sendTime = parseJsonMessage(collector.graph, await request.body())
 
        collector.setAttrs(client, clientSession, settings, sendTime)
 
        return Response('', status_code=202)
 

	
 

	
 
def main():
 
    logging.getLogger('autodepgraphapi').setLevel(logging.INFO)
 
    logging.getLogger('syncedgraph').setLevel(logging.INFO)
 
    logging.getLogger('output.allDmx').setLevel(logging.WARNING)
 

	
 
    graph = SyncedGraph(networking.rdfdb.url, "collector")
 

	
 
    try:
 
        # todo: drive outputs with config files
 
        rate = 30
 
        outputs: List[Output] = [
 
            # ArtnetDmx(L9['output/dmxA/'],
 
            #           host='127.0.0.1',
 
            #           port=6445,
 
            #           rate=rate),
 
            #sudo chmod a+rw /dev/bus/usb/003/021
 
            Udmx(L9['output/dmxA/'], bus=3, address=21, lastDmxChannel=100),
 
        ]
 
    except Exception:
 
        log.error("setting up outputs:")
light9/effect/effecteval.py
Show inline comments
 
import logging
 
import math
 
import random
 
from colorsys import hsv_to_rgb
 
from dataclasses import dataclass
 
from typing import Dict, Tuple
 

	
 
from noise import pnoise1
 
from PIL import Image
 
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, EffectSettings
 
from light9.effect.settings import BareEffectSettings, DeviceSettings, EffectSettings
 
from light9.effect.simple_outputs import SimpleOutputs
 
from light9.namespaces import DEV, L9
 
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(bnorm * 255))))
 

	
 

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

	
 

	
 
def nsin(x):
 
@@ -59,75 +59,77 @@ def noise(t):
 
def clamp(lo, hi, x):
 
    return max(lo, min(hi, x))
 

	
 

	
 
def clamp255(x):
 
    return min(255, max(0, x))
 

	
 

	
 
def _8bit(f):
 
    if not isinstance(f, (int, float)):
 
        raise TypeError(repr(f))
 
    return clamp255(int(f * 255))
 

	
 

	
 
@dataclass
 
class EffectEval:
 
    """
 
    runs one effect's code to turn effect attr settings into output
 
    device settings. No state; suitable for reload().
 
    """
 
    graph: SyncedGraph
 
    effect: EffectClass
 
    simpleOutputs: SimpleOutputs
 

	
 
    def outputFromEffect(self, effectSettings: BareEffectSettings, songTime: float, noteTime: float) -> Tuple[EffectSettings, Dict]:
 
    def outputFromEffect(self, effectSettings: BareEffectSettings, songTime: float, noteTime: float) -> Tuple[DeviceSettings, Dict]:
 
        """
 
        From effect attr settings, like strength=0.75, to output device
 
        settings like light1/bright=0.72;light2/bright=0.78. This runs
 
        the effect code.
 
        """
 
        # todo: what does the next comment line mean?
 
        # both callers need to apply note overrides
 

	
 
        strength = float(effectSettings.s[EffectAttr(L9['strength'])])
 
        if strength <= 0:
 
            return EffectSettings(self.graph, []), {'zero': True}
 
            return DeviceSettings(self.graph, []), {'zero': True}
 

	
 
        report = {}
 
        out: Dict[Tuple[DeviceUri, DeviceAttr], VTUnion] = {}  # (dev, attr): value
 
        out: Dict[Tuple[DeviceUri, DeviceAttr], VTUnion] = {}
 

	
 
        out.update(self.simpleOutputs.values(self.effect, strength, effectSettings.s.get(EffectAttr(L9['colorScale']), None)))
 

	
 
        if self.effect.startswith(L9['effect/']):
 
            tail = 'effect_' + self.effect[len(L9['effect/']):]
 
            try:
 
                func = globals()[tail]
 
            except KeyError:
 
                report['error'] = 'effect code not found for %s' % self.effect
 
            else:
 
                out.update(func(effectSettings, strength, songTime, noteTime))
 

	
 
        outList = [(d, a, v) for (d, a), v in out.items()]
 
        return EffectSettings(self.graph, outList), report
 
        return DeviceSettings(self.graph, outList), report
 

	
 

	
 
def effect_Curtain(effectSettings, strength, songTime, noteTime):
 
    return {(L9['device/lowPattern%s' % n], L9['color']): literalColor(strength, strength, strength) for n in range(301, 308 + 1)}
 

	
 

	
 
def effect_animRainbow(effectSettings, strength, songTime, noteTime):
 
    out = {}
 
    tint = effectSettings.get(L9['tint'], '#ffffff')
 
    tintStrength = float(effectSettings.get(L9['tintStrength'], 0))
 
    tr, tg, tb = hex_to_rgb(tint)
 
    for n in range(1, 5 + 1):
 
        scl = strength * nsin(songTime + n * .3)**3
 
        col = literalColor(scl * lerp(nsin(songTime + n * .2), tr / 255, tintStrength), scl * lerp(nsin(songTime + n * .2 + .3), tg / 255, tintStrength),
 
                           scl * lerp(nsin(songTime + n * .3 + .6), tb / 255, tintStrength))
 

	
 
        dev = L9['device/aura%s' % n]
 
        out.update({
 
            (dev, L9['color']): col,
 
            (dev, L9['zoom']): .9,
 
        })
 
        ang = songTime * 4
 
        out.update({
 
            (dev, L9['rx']): lerp(.27, .7, (n - 1) / 4) + .2 * math.sin(ang + n),
light9/effect/effecteval_test.py
Show inline comments
 
new file 100644
 
import time
 
import unittest
 

	
 
from freezegun import freeze_time
 
from light9.effect.effecteval import EffectEval
 
from light9.effect.settings import BareEffectSettings, DeviceSettings, EffectSettings
 
from light9.effect.simple_outputs import SimpleOutputs
 
from rdflib import Namespace
 

	
 
from light9.collector.collector import Collector
 
from light9.collector.output import Output
 
from light9.collector.weblisteners import WebListeners
 
from light9.mock_syncedgraph import MockSyncedGraph
 
from light9.namespaces import DEV, L9
 
from light9.newtypes import (ClientSessionType, ClientType, DeviceAttr, DeviceUri, EffectAttr, EffectClass, HexColor, UnixTime)
 

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

	
 
THEATER = '''
 

	
 
    :effectClass1 a :EffectClass .
 
#add light to here
 
'''
 
effectClass1 = EffectClass(L9['effectClass1'])
 

	
 

	
 
class TestEffectEval:
 

	
 
    def test_scalesColors(self):
 
        g = MockSyncedGraph(THEATER)
 
        so = SimpleOutputs(g)
 
        ee = EffectEval(g, effectClass1, so)
 
        s = BareEffectSettings(s={EffectAttr(L9['strength']): 0.5})
 
        ds,report = ee.outputFromEffect(s, songTime=0, noteTime=0)
 
        assert ds == DeviceSettings(g, [(DeviceUri(L9['light1']), DeviceAttr(L9['color']), HexColor('#888888'))])
0 comments (0 inline, 0 general)