diff --git a/src/light9/blender/light_control/send_to_collector.py b/src/light9/blender/light_control/send_to_collector.py --- a/src/light9/blender/light_control/send_to_collector.py +++ b/src/light9/blender/light_control/send_to_collector.py @@ -1,17 +1,20 @@ import asyncio import logging from dataclasses import dataclass, field +from os import sync +from typing import cast import bpy # type: ignore from light9.blender.time_sync.time_from_graph import clamp from light9.effect.effecteval import literalColor from rdfdb.patch import Patch from rdfdb.syncedgraph.syncedgraph import SyncedGraph -from rdflib import Literal, URIRef +from rdflib import RDF, ConjunctiveGraph, Literal, URIRef +from rdflib.graph import _ContextType from light9 import networking from light9.blender.asyncio_thread import startLoopInThread -from light9.namespaces import L9 +from light9.namespaces import FUNC, L9 from light9.newtypes import DeviceUri from light9.showconfig import showUri @@ -20,19 +23,34 @@ log = logging.getLogger('sendcoll') UPDATE_PERIOD = 1 / 20 +async def waitForConnection(graph: SyncedGraph): + # workaround for patch() quietly failing before connection. See SyncedGraph. + while not graph._isConnected(): + await asyncio.sleep(.5) + @dataclass class BlenderDev: ctx: URIRef obj: bpy.types.Object uri: DeviceUri + setting: URIRef + devSets: URIRef + color: tuple[float, float, float] = (0, 0, 0) def updateColor(self): self.color = self.obj.data.color + def effectGraphStmts(self, bset0) -> list: + return [ + (self.devSets, L9['setting'], self.setting, self.ctx), + (self.setting, L9['device'], self.uri, self.ctx), + (self.setting, L9['deviceAttr'], L9['color'], self.ctx), + ] + def makePatch(self, graph: SyncedGraph) -> Patch: c = literalColor(*[clamp(x, 0, 1) for x in self.color]) - return graph.getObjectPatch(self.ctx, self.uri, L9['blenderColor'], c) + return graph.getObjectPatch(self.ctx, self.setting, L9['value'], c) @dataclass @@ -40,31 +58,70 @@ class Sender: syncedObjects: dict[str, BlenderDev] = field(default_factory=dict) ctx = URIRef(showUri() + '/blender') + devSets = L9['blenderControlDevSets'] def __post_init__(self): bpy.app.timers.register(self.findLightsInScene, first_interval=0.1) # todo: what event to use? startLoopInThread(self.task()) async def task(self): - graph = SyncedGraph(networking.rdfdb.url, "blender_light_control") - while True: - try: - p = Patch(addQuads=[], delQuads=[]) - for d in self.syncedObjects.values(): - p = p.update(d.makePatch(graph)) - if p: - await graph.patch(p) - except Exception: - log.error('skip', exc_info=True) - await asyncio.sleep(1) - await asyncio.sleep(UPDATE_PERIOD) + log.info('start task') + try: + self.graph = SyncedGraph(networking.rdfdb.url, "blender_light_control") + await waitForConnection(self.graph) + self.patchInitialGraph() + + while True: + try: + await self.patchCurrentLightColors() + except Exception: + log.error('skip', exc_info=True) + await asyncio.sleep(1) + # todo: this could be triggered by onColorChanges instead of running constantly + await asyncio.sleep(UPDATE_PERIOD) + except Exception: + log.error('task', exc_info=True) + + def patchInitialGraph(self): + g = self.effectGraph() + for d in self.syncedObjects.values(): + for stmt in d.effectGraphStmts(self.devSets): + g.add(stmt) + for stmt in g: + log.info("adding %s", stmt) + self.graph.patchSubgraph(self.ctx, g) + + async def patchCurrentLightColors(self): + p = Patch(addQuads=[], delQuads=[]) + for d in self.syncedObjects.values(): + p = p.update(d.makePatch(self.graph)) + if p: + await self.graph.patch(p) + + def effectGraph(self) -> ConjunctiveGraph: + g = ConjunctiveGraph() + ctx = cast(_ContextType, self.ctx) + g.add((L9['blenderControl'], RDF.type, L9['Effect'], ctx)) + g.add((L9['blenderControl'], L9['effectFunction'], FUNC['scale'], ctx)) + g.add((L9['blenderControl'], L9['publishAttr'], L9['strength'], ctx)) + setting = L9['blenderControl/set0'] + g.add((L9['blenderControl'], L9['setting'], setting, ctx)) + g.add((setting, L9['value'], self.devSets, ctx)) + g.add((setting, L9['effectAttr'], L9['deviceSettings'], ctx)) + return g def findLightsInScene(self): self.syncedObjects.clear() - for obj in bpy.data.objects: + for obj in sorted(bpy.data.objects, key=lambda x: x.name): if obj.get('l9dev'): uri = DeviceUri(URIRef(obj['l9dev'])) - self.syncedObjects[obj.name] = BlenderDev(self.ctx, obj, uri) + self.syncedObjects[obj.name] = BlenderDev( + self.ctx, + obj, + uri, + setting=L9['blenderControl/set1'], + devSets=self.devSets, + ) log.info(f'found {self.syncedObjects[obj.name]}') self.watchForUpdates() diff --git a/src/light9/blender/time_sync/time_from_graph.py b/src/light9/blender/time_sync/time_from_graph.py --- a/src/light9/blender/time_sync/time_from_graph.py +++ b/src/light9/blender/time_sync/time_from_graph.py @@ -50,7 +50,7 @@ class Sync: self.wallStartTime = typedValue(float | None, self.graph, asco, L9['wallStartTime']) self.pausedSongTime = typedValue(float | None, self.graph, asco, L9['pausedSongTime']) self.duration = typedValue(float | None, self.graph, asco, L9['duration']) or 1.0 - self.playing = typedValue(bool|None, self.graph, asco, L9['playing']) or False + self.playing = typedValue(bool | None, self.graph, asco, L9['playing']) or False def currentTime(self) -> float | None: if self.wallStartTime is not None: