diff blender/light_control/send_to_collector.py @ 2453:b23afde50bc2

blender addons get thier own pdm setup for now. fix time_from_graph startup race
author drewp@bigasterisk.com
date Sun, 18 May 2025 20:08:35 -0700
parents src/light9/blender/light_control/send_to_collector.py@e683b449506b
children 2d454737a916
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/blender/light_control/send_to_collector.py	Sun May 18 20:08:35 2025 -0700
@@ -0,0 +1,138 @@
+import asyncio
+import logging
+from dataclasses import dataclass, field
+from os import sync
+from typing import cast
+
+import bpy  # type: ignore
+from light9_sync.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 RDF, ConjunctiveGraph, Literal, URIRef
+from rdflib.graph import _ContextType
+
+from light9 import networking
+from light9_sync.asyncio_thread import startLoopInThread
+from light9.namespaces import FUNC, L9
+from light9.newtypes import DeviceUri
+from light9.showconfig import showUri
+
+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.setting, L9['value'], c)
+
+
+@dataclass
+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):
+        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 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,
+                    setting=L9['blenderControl/set1'],
+                    devSets=self.devSets,
+                )
+                log.info(f'found {self.syncedObjects[obj.name]}')
+
+        self.watchForUpdates()
+
+    def watchForUpdates(self):
+        bpy.app.handlers.depsgraph_update_post.append(self.onColorChanges)
+        bpy.app.handlers.frame_change_post.append(self.onColorChanges)
+
+    def onColorChanges(self, scene, deps):
+        for obj in deps.objects:
+            dev = self.syncedObjects.get(obj.name)
+            if dev is None:
+                continue
+            dev.updateColor()