Mercurial > code > home > repos > light9
comparison 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 |
comparison
equal
deleted
inserted
replaced
2452:f21b61884b0f | 2453:b23afde50bc2 |
---|---|
1 import asyncio | |
2 import logging | |
3 from dataclasses import dataclass, field | |
4 from os import sync | |
5 from typing import cast | |
6 | |
7 import bpy # type: ignore | |
8 from light9_sync.time_sync.time_from_graph import clamp | |
9 from light9.effect.effecteval import literalColor | |
10 from rdfdb.patch import Patch | |
11 from rdfdb.syncedgraph.syncedgraph import SyncedGraph | |
12 from rdflib import RDF, ConjunctiveGraph, Literal, URIRef | |
13 from rdflib.graph import _ContextType | |
14 | |
15 from light9 import networking | |
16 from light9_sync.asyncio_thread import startLoopInThread | |
17 from light9.namespaces import FUNC, L9 | |
18 from light9.newtypes import DeviceUri | |
19 from light9.showconfig import showUri | |
20 | |
21 log = logging.getLogger('sendcoll') | |
22 | |
23 UPDATE_PERIOD = 1 / 20 | |
24 | |
25 | |
26 async def waitForConnection(graph: SyncedGraph): | |
27 # workaround for patch() quietly failing before connection. See SyncedGraph. | |
28 while not graph._isConnected(): | |
29 await asyncio.sleep(.5) | |
30 | |
31 @dataclass | |
32 class BlenderDev: | |
33 ctx: URIRef | |
34 obj: bpy.types.Object | |
35 uri: DeviceUri | |
36 setting: URIRef | |
37 devSets: URIRef | |
38 | |
39 color: tuple[float, float, float] = (0, 0, 0) | |
40 | |
41 def updateColor(self): | |
42 self.color = self.obj.data.color | |
43 | |
44 def effectGraphStmts(self, bset0) -> list: | |
45 return [ | |
46 (self.devSets, L9['setting'], self.setting, self.ctx), | |
47 (self.setting, L9['device'], self.uri, self.ctx), | |
48 (self.setting, L9['deviceAttr'], L9['color'], self.ctx), | |
49 ] | |
50 | |
51 def makePatch(self, graph: SyncedGraph) -> Patch: | |
52 c = literalColor(*[clamp(x, 0, 1) for x in self.color]) | |
53 return graph.getObjectPatch(self.ctx, self.setting, L9['value'], c) | |
54 | |
55 | |
56 @dataclass | |
57 class Sender: | |
58 syncedObjects: dict[str, BlenderDev] = field(default_factory=dict) | |
59 | |
60 ctx = URIRef(showUri() + '/blender') | |
61 devSets = L9['blenderControlDevSets'] | |
62 | |
63 def __post_init__(self): | |
64 bpy.app.timers.register(self.findLightsInScene, first_interval=0.1) # todo: what event to use? | |
65 startLoopInThread(self.task()) | |
66 | |
67 async def task(self): | |
68 log.info('start task') | |
69 try: | |
70 self.graph = SyncedGraph(networking.rdfdb.url, "blender_light_control") | |
71 await waitForConnection(self.graph) | |
72 self.patchInitialGraph() | |
73 | |
74 while True: | |
75 try: | |
76 await self.patchCurrentLightColors() | |
77 except Exception: | |
78 log.error('skip', exc_info=True) | |
79 await asyncio.sleep(1) | |
80 # todo: this could be triggered by onColorChanges instead of running constantly | |
81 await asyncio.sleep(UPDATE_PERIOD) | |
82 except Exception: | |
83 log.error('task', exc_info=True) | |
84 | |
85 def patchInitialGraph(self): | |
86 g = self.effectGraph() | |
87 for d in self.syncedObjects.values(): | |
88 for stmt in d.effectGraphStmts(self.devSets): | |
89 g.add(stmt) | |
90 for stmt in g: | |
91 log.info("adding %s", stmt) | |
92 self.graph.patchSubgraph(self.ctx, g) | |
93 | |
94 async def patchCurrentLightColors(self): | |
95 p = Patch(addQuads=[], delQuads=[]) | |
96 for d in self.syncedObjects.values(): | |
97 p = p.update(d.makePatch(self.graph)) | |
98 if p: | |
99 await self.graph.patch(p) | |
100 | |
101 def effectGraph(self) -> ConjunctiveGraph: | |
102 g = ConjunctiveGraph() | |
103 ctx = cast(_ContextType, self.ctx) | |
104 g.add((L9['blenderControl'], RDF.type, L9['Effect'], ctx)) | |
105 g.add((L9['blenderControl'], L9['effectFunction'], FUNC['scale'], ctx)) | |
106 g.add((L9['blenderControl'], L9['publishAttr'], L9['strength'], ctx)) | |
107 setting = L9['blenderControl/set0'] | |
108 g.add((L9['blenderControl'], L9['setting'], setting, ctx)) | |
109 g.add((setting, L9['value'], self.devSets, ctx)) | |
110 g.add((setting, L9['effectAttr'], L9['deviceSettings'], ctx)) | |
111 return g | |
112 | |
113 def findLightsInScene(self): | |
114 self.syncedObjects.clear() | |
115 for obj in sorted(bpy.data.objects, key=lambda x: x.name): | |
116 if obj.get('l9dev'): | |
117 uri = DeviceUri(URIRef(obj['l9dev'])) | |
118 self.syncedObjects[obj.name] = BlenderDev( | |
119 self.ctx, | |
120 obj, | |
121 uri, | |
122 setting=L9['blenderControl/set1'], | |
123 devSets=self.devSets, | |
124 ) | |
125 log.info(f'found {self.syncedObjects[obj.name]}') | |
126 | |
127 self.watchForUpdates() | |
128 | |
129 def watchForUpdates(self): | |
130 bpy.app.handlers.depsgraph_update_post.append(self.onColorChanges) | |
131 bpy.app.handlers.frame_change_post.append(self.onColorChanges) | |
132 | |
133 def onColorChanges(self, scene, deps): | |
134 for obj in deps.objects: | |
135 dev = self.syncedObjects.get(obj.name) | |
136 if dev is None: | |
137 continue | |
138 dev.updateColor() |